smart pointer cleanup
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / editorActions / enter / EnterAfterUnmatchedBraceHandler.java
1 /*
2  * Copyright 2000-2013 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.Ref;
35 import com.intellij.openapi.util.TextRange;
36 import com.intellij.openapi.util.text.StringUtil;
37 import com.intellij.psi.PsiDocumentManager;
38 import com.intellij.psi.PsiElement;
39 import com.intellij.psi.PsiFile;
40 import com.intellij.psi.TokenType;
41 import com.intellij.psi.codeStyle.CodeStyleManager;
42 import com.intellij.psi.tree.IElementType;
43 import com.intellij.psi.util.PsiUtilCore;
44 import com.intellij.util.IncorrectOperationException;
45 import com.intellij.util.text.CharArrayUtil;
46 import org.jetbrains.annotations.NotNull;
47
48 public class EnterAfterUnmatchedBraceHandler extends EnterHandlerDelegateAdapter {
49   private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.editorActions.enter.EnterAfterUnmatchedBraceHandler");
50
51   @Override
52   public Result preprocessEnter(@NotNull final PsiFile file, @NotNull final Editor editor, @NotNull final Ref<Integer> caretOffsetRef, @NotNull final Ref<Integer> caretAdvance,
53                                 @NotNull final DataContext dataContext, final EditorActionHandler originalHandler) {
54     Document document = editor.getDocument();
55     CharSequence text = document.getCharsSequence();
56     Project project = file.getProject();
57     int caretOffset = caretOffsetRef.get().intValue();
58     int unmatchedLBracesNumber = getUnmatchedLBracesNumberBefore(editor, caretOffset, file.getFileType());
59     if (!CodeInsightSettings.getInstance().INSERT_BRACE_ON_ENTER || unmatchedLBracesNumber <= 0) {
60       return Result.Continue;
61     }
62     
63     int offset = CharArrayUtil.shiftForward(text, caretOffset, " \t");
64     if (offset < document.getTextLength()) {
65       char c = text.charAt(offset);
66       if (c != ')' && c != ']' && c != ';' && c != ',' && c != '%' && c != '<' && c != '?') {
67         offset = calculateOffsetToInsertClosingBrace(file, text, offset);
68         //offset = CharArrayUtil.shiftForwardUntil(text, caretOffset, "\n");
69       }
70     }
71     offset = Math.min(offset, document.getTextLength());
72
73     // We need to adjust indents of the text that will be moved, hence, need to insert preliminary line feed.
74     // Example:
75     //     if (test1()) {
76     //     } else {<caret> if (test2()) {
77     //         foo();
78     //     }
79     // We insert here '\n}' after 'foo();' and have the following:
80     //     if (test1()) {
81     //     } else { if (test2()) {
82     //         foo();
83     //         }
84     //     }
85     // That is formatted incorrectly because line feed between 'else' and 'if' is not inserted yet (whole 'if' block is indent anchor
86     // to 'if' code block('{}')). So, we insert temporary line feed between 'if' and 'else', correct indent and remove that temporary
87     // line feed.
88     int bracesToInsert = 0;
89     outer:
90     for (int i = caretOffset - 1; unmatchedLBracesNumber > 0 && i >= 0 && bracesToInsert < unmatchedLBracesNumber; i--) {
91       char c = text.charAt(i);
92       switch (c) {
93         case ' ':
94         case '\n':
95         case '\t':
96           continue;
97         case '{': bracesToInsert++; break;
98         default: break outer;
99       }
100     }
101     bracesToInsert = Math.max(bracesToInsert, 1);
102     document.insertString(offset, "\n" + StringUtil.repeatSymbol('}', bracesToInsert));
103     document.insertString(caretOffset, "\n");
104     PsiDocumentManager.getInstance(project).commitDocument(document);
105     long stamp = document.getModificationStamp();
106     boolean closingBraceIndentAdjusted;
107     try {
108       CodeStyleManager.getInstance(project).adjustLineIndent(file, new TextRange(caretOffset, offset + 2));
109     }
110     catch (IncorrectOperationException e) {
111       LOG.error(e);
112     }
113     finally {
114       closingBraceIndentAdjusted = stamp != document.getModificationStamp();
115       document.deleteString(caretOffset, caretOffset + 1);
116     }
117
118     // There is a possible case that formatter was unable to adjust line indent for the closing brace (that is the case for plain text
119     // document for example). Hence, we're trying to do the manually.
120     if (!closingBraceIndentAdjusted) {
121       int line = document.getLineNumber(offset);
122       StringBuilder buffer = new StringBuilder();
123       int start = document.getLineStartOffset(line);
124       int end = document.getLineEndOffset(line);
125       for (int i = start; i < end; i++) {
126         char c = text.charAt(i);
127         if (c != ' ' && c != '\t') {
128           break;
129         }
130         else {
131           buffer.append(c);
132         }
133       }
134       if (buffer.length() > 0) {
135         document.insertString(offset + 1, buffer);
136       }
137     }
138
139     return Result.DefaultForceIndent;
140   }
141
142   /**
143    * Current handler inserts closing curly brace (right brace) if necessary. There is a possible case that it should be located
144    * more than one line forward.
145    * <p/>
146    * <b>Example</b> 
147    * <pre>
148    *     if (test1()) {
149    *     } else {<caret> if (test2()) {
150    *         foo();
151    *     }
152    * </pre>
153    * <p/>
154    * We want to get this after the processing:
155    * <pre>
156    *     if (test1()) {
157    *     } else {
158    *         if (test2()) {
159    *             foo();
160    *         }
161    *     }
162    * </pre>
163    * I.e. closing brace should be inserted two lines below current caret line. Hence, we need to calculate correct offset
164    * to use for brace inserting. This method is responsible for that.
165    * <p/>
166    * In essence it inspects PSI structure and finds PSE elements with the max length that starts at caret offset. End offset
167    * of that element is used as an insertion point.
168    * 
169    * @param file    target PSI file
170    * @param text    text from the given file
171    * @param offset  target offset where line feed will be inserted
172    * @return        offset to use for inserting closing brace
173    */
174   protected int calculateOffsetToInsertClosingBrace(PsiFile file, CharSequence text, final int offset) {
175     PsiElement element = PsiUtilCore.getElementAtOffset(file, offset);
176     ASTNode node = element.getNode();
177     if (node != null && node.getElementType() == TokenType.WHITE_SPACE) {
178       return CharArrayUtil.shiftForwardUntil(text, offset, "\n");
179     }
180     for (PsiElement parent = element.getParent(); parent != null; parent = parent.getParent()) {
181       ASTNode parentNode = parent.getNode();
182       if (parentNode == null || parentNode.getStartOffset() != offset) {
183         break;
184       }
185       element = parent;
186     }
187     if (element.getTextOffset() != offset) {
188       return CharArrayUtil.shiftForwardUntil(text, offset, "\n");
189     }
190     else {
191       return element.getTextRange().getEndOffset();
192     }
193   }
194   
195   public static boolean isAfterUnmatchedLBrace(Editor editor, int offset, FileType fileType) {
196     return getUnmatchedLBracesNumberBefore(editor, offset, fileType) > 0;
197   }
198
199   /**
200    * Calculates number of unmatched left braces before the given offset.
201    * 
202    * @param editor    target editor
203    * @param offset    target offset
204    * @param fileType  target file type
205    * @return          number of unmatched braces before the given offset;
206    *                  negative value if it's not possible to perform the calculation or if there are no unmatched left braces before
207    *                  the given offset
208    */
209   protected static int getUnmatchedLBracesNumberBefore(Editor editor, int offset, FileType fileType) {
210     if (offset == 0) {
211       return -1;
212     }
213     CharSequence chars = editor.getDocument().getCharsSequence();
214     if (chars.charAt(offset - 1) != '{') {
215       return -1;
216     }
217
218     EditorHighlighter highlighter = ((EditorEx)editor).getHighlighter();
219     HighlighterIterator iterator = highlighter.createIterator(offset - 1);
220     BraceMatcher braceMatcher = BraceMatchingUtil.getBraceMatcher(fileType, iterator);
221
222     if (!braceMatcher.isLBraceToken(iterator, chars, fileType) || !braceMatcher.isStructuralBrace(iterator, chars, fileType)) {
223       return -1;
224     }
225
226     Language language = iterator.getTokenType().getLanguage();
227
228     iterator = highlighter.createIterator(0);
229     int lBracesBeforeOffset = 0;
230     int lBracesAfterOffset = 0;
231     int rBracesBeforeOffset = 0;
232     int rBracesAfterOffset = 0;
233     for (; !iterator.atEnd(); iterator.advance()) {
234       IElementType tokenType = iterator.getTokenType();
235       if (!tokenType.getLanguage().equals(language) || !braceMatcher.isStructuralBrace(iterator, chars, fileType)) {
236         continue;
237       }
238
239       boolean beforeOffset = iterator.getStart() < offset;
240       
241       if (braceMatcher.isLBraceToken(iterator, chars, fileType)) {
242         if (beforeOffset) {
243           lBracesBeforeOffset++;
244         }
245         else {
246           lBracesAfterOffset++;
247         }
248       }
249       else if (braceMatcher.isRBraceToken(iterator, chars, fileType)) {
250         if (beforeOffset) {
251           rBracesBeforeOffset++;
252         }
253         else {
254           rBracesAfterOffset++;
255         }
256       }
257     }
258     
259     return lBracesBeforeOffset - rBracesBeforeOffset - (rBracesAfterOffset - lBracesAfterOffset);
260   }
261 }