2 * Copyright 2000-2015 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.intellij.codeInsight.editorActions.enter;
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;
49 public class EnterAfterUnmatchedBraceHandler extends EnterHandlerDelegateAdapter {
50 private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.editorActions.enter.EnterAfterUnmatchedBraceHandler");
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) {
60 int caretOffset = caretOffsetRef.get();
61 if (!isApplicable(file, caretOffset)) {
62 return Result.Continue;
65 int maxRBraceCount = getMaxRBraceCount(file, editor, caretOffset);
66 if (maxRBraceCount > 0) {
67 insertRBraces(file, editor,
69 getRBraceOffset(file, editor, caretOffset),
70 adjustRBraceCountForPosition(editor, caretOffset, maxRBraceCount));
71 return Result.DefaultForceIndent;
73 return Result.Continue;
77 * Checks that the text context is in responsibility of the handler.
79 * @param file target PSI file
80 * @param caretOffset target caret offset
81 * @return true, if handler is in charge
83 public boolean isApplicable(@NotNull PsiFile file, int caretOffset) {
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.
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
97 protected int getMaxRBraceCount(@NotNull final PsiFile file, @NotNull final Editor editor, int caretOffset) {
98 if (!CodeInsightSettings.getInstance().INSERT_BRACE_ON_ENTER) {
101 return Math.max(0, getUnmatchedLBracesNumberBefore(editor, caretOffset, file.getFileType()));
105 * Calculates the string of '}' that have to be inserted by handler.
106 * Some languages can expand the string by additional characters (i.e. '\', ';')
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 '}'
113 protected String adjustRBraceCountForPosition(@NotNull final Editor editor, int caretOffset, int maxRBraceCount) {
114 assert maxRBraceCount > 0;
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);
123 else if (isStopChar(c)) {
127 return StringUtil.repeatSymbol('}', Math.max(bracesToInsert, 1));
131 * Checks the character before the inserted '}' to reduce the count of inserted '}'.
132 * The number of inserted '}' will increase for each found '{'.
134 * @param c character to check
135 * @return true, to stop back iteration
137 protected boolean isStopChar(char c) {
138 return " \n\t".indexOf(c) < 0;
142 * Calculates the position for insertion of one or more '}'.
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
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");
157 return Math.min(offset, fileLength);
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
168 protected void insertRBraces(@NotNull PsiFile file,
169 @NotNull Editor editor,
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);
179 * Inserts the <code>rBracesCount</code> of '}' at the <code>rBracesInsertOffset</code> position.
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
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.
191 // } else {<caret> if (test2()) {
194 // We insert here '\n}' after 'foo();' and have the following:
196 // } else { if (test2()) {
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
203 document.insertString(caretOffset, "\n");
207 * Formats the code block between caret and inserted braces.
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
215 protected void formatCodeFragmentBetweenBraces(@NotNull PsiFile file,
216 @NotNull Document document,
218 int rBracesInsertOffset,
219 String generatedRBraces) {
220 Project project = file.getProject();
221 long stamp = document.getModificationStamp();
222 boolean closingBraceIndentAdjusted;
224 PsiDocumentManager.getInstance(project).commitDocument(document);
225 CodeStyleManager.getInstance(project).adjustLineIndent(file, new TextRange(caretOffset, rBracesInsertOffset + 2));
227 catch (IncorrectOperationException e) {
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);
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') {
253 if (buffer.length() > 0) {
254 document.insertString(rBracesInsertOffset + 1, buffer);
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.
266 * } else {<caret> if (test2()) {
271 * We want to get this after the processing:
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.
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.
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
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"));
297 for (PsiElement parent = element.getParent(); parent != null; parent = parent.getParent()) {
298 ASTNode parentNode = parent.getNode();
299 if (parentNode == null || parentNode.getStartOffset() != offset) {
304 if (element.getTextOffset() != offset) {
305 return Pair.create(null, CharArrayUtil.shiftForwardUntil(text, offset, "\n"));
307 return Pair.create(element, element.getTextRange().getEndOffset());
310 public static boolean isAfterUnmatchedLBrace(Editor editor, int offset, FileType fileType) {
311 return getUnmatchedLBracesNumberBefore(editor, offset, fileType) > 0;
315 * Calculates number of unmatched left braces before the given offset.
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
324 protected static int getUnmatchedLBracesNumberBefore(Editor editor, int offset, FileType fileType) {
328 CharSequence chars = editor.getDocument().getCharsSequence();
329 if (chars.charAt(offset - 1) != '{') {
333 EditorHighlighter highlighter = ((EditorEx)editor).getHighlighter();
334 HighlighterIterator iterator = highlighter.createIterator(offset - 1);
335 BraceMatcher braceMatcher = BraceMatchingUtil.getBraceMatcher(fileType, iterator);
337 if (!braceMatcher.isLBraceToken(iterator, chars, fileType) || !braceMatcher.isStructuralBrace(iterator, chars, fileType)) {
341 Language language = iterator.getTokenType().getLanguage();
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)) {
354 boolean beforeOffset = iterator.getStart() < offset;
356 if (braceMatcher.isLBraceToken(iterator, chars, fileType)) {
358 lBracesBeforeOffset++;
361 lBracesAfterOffset++;
364 else if (braceMatcher.isRBraceToken(iterator, chars, fileType)) {
366 rBracesBeforeOffset++;
369 rBracesAfterOffset++;
374 return lBracesBeforeOffset - rBracesBeforeOffset - (rBracesAfterOffset - lBracesAfterOffset);