extract java closure folding into a separate class with smaller methods
[idea/community.git] / java / java-psi-impl / src / com / intellij / codeInsight / folding / impl / ClosureFolding.java
1 /*
2  * Copyright 2000-2016 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.codeInsight.folding.impl;
17
18 import com.intellij.codeInsight.daemon.impl.analysis.HighlightUtilBase;
19 import com.intellij.codeInsight.generation.OverrideImplementExploreUtil;
20 import com.intellij.lang.folding.NamedFoldingDescriptor;
21 import com.intellij.openapi.editor.Document;
22 import com.intellij.openapi.editor.FoldingGroup;
23 import com.intellij.openapi.project.IndexNotReadyException;
24 import com.intellij.openapi.util.text.StringUtil;
25 import com.intellij.psi.*;
26 import com.intellij.psi.util.PsiUtil;
27 import com.intellij.util.Function;
28 import com.intellij.util.ObjectUtils;
29 import com.intellij.util.text.CharArrayUtil;
30 import org.jetbrains.annotations.NotNull;
31 import org.jetbrains.annotations.Nullable;
32
33 import java.util.ArrayList;
34 import java.util.List;
35
36 /**
37  * @author peter
38  */
39 class ClosureFolding {
40   @NotNull private final PsiAnonymousClass myAnonymousClass;
41   @NotNull private final PsiNewExpression myNewExpression;
42   @Nullable private final PsiClass myBaseClass;
43   @NotNull private final JavaFoldingBuilderBase myBuilder;
44   @NotNull private final PsiMethod myMethod;
45   @NotNull final PsiCodeBlock methodBody;
46   private final boolean myQuick;
47
48   private ClosureFolding(@NotNull PsiAnonymousClass anonymousClass,
49                         @NotNull PsiNewExpression newExpression,
50                         boolean quick,
51                         @Nullable PsiClass baseClass,
52                         @NotNull JavaFoldingBuilderBase builder,
53                         @NotNull PsiMethod method,
54                         @NotNull PsiCodeBlock methodBody) {
55     myAnonymousClass = anonymousClass;
56     myNewExpression = newExpression;
57     myQuick = quick;
58     myBaseClass = baseClass;
59     myBuilder = builder;
60     myMethod = method;
61     this.methodBody = methodBody;
62   }
63
64   @Nullable
65   List<NamedFoldingDescriptor> process(Document document) {
66     PsiJavaToken lbrace = methodBody.getLBrace();
67     PsiJavaToken rbrace = methodBody.getRBrace();
68     PsiElement classRBrace = myAnonymousClass.getRBrace();
69     if (lbrace == null || rbrace == null || classRBrace == null) return null;
70
71     CharSequence seq = document.getCharsSequence();
72     int rangeStart = lbrace.getTextRange().getEndOffset();
73     int rangeEnd = getContentRangeEnd(document, rbrace, classRBrace);
74
75     String contents = getClosureContents(rangeStart, rangeEnd, seq);
76     if (contents == null) return null;
77
78     String methodName = shouldShowMethodName() ? myMethod.getName() : "";
79     if (StringUtil.isEmpty(methodName) && PsiUtil.isLanguageLevel8OrHigher(myAnonymousClass)) return null;
80
81     String header = getFoldingHeader(methodName);
82     if (showSingleLineFolding(document, contents, header)) {
83       return createDescriptors(classRBrace, trimStartSpaces(seq, rangeStart), trimTailSpaces(seq, rangeEnd), header + " ", " }");
84     }
85
86     return createDescriptors(classRBrace, rangeStart, rangeEnd, header, "}");
87   }
88
89   private static int trimStartSpaces(CharSequence seq, int rangeStart) {
90     return CharArrayUtil.shiftForward(seq, rangeStart, " \n\t");
91   }
92
93   private static int trimTailSpaces(CharSequence seq, int rangeEnd) {
94     return CharArrayUtil.shiftBackward(seq, rangeEnd - 1, " \n\t") + 1;
95   }
96
97   private static int getContentRangeEnd(Document document, PsiJavaToken rbrace, PsiElement classRBrace) {
98     CharSequence seq = document.getCharsSequence();
99     int rangeEnd = rbrace.getTextRange().getStartOffset();
100
101     int methodEndLine = document.getLineNumber(rangeEnd);
102     int methodEndLineStart = document.getLineStartOffset(methodEndLine);
103     if ("}".equals(seq.subSequence(methodEndLineStart, document.getLineEndOffset(methodEndLine)).toString().trim())) {
104       int classEndStart = classRBrace.getTextRange().getStartOffset();
105       int classEndCol = classEndStart - document.getLineStartOffset(document.getLineNumber(classEndStart));
106       return classEndCol + methodEndLineStart;
107     }
108     return rangeEnd;
109   }
110
111   private boolean showSingleLineFolding(Document document, String contents, String header) {
112     return contents.indexOf('\n') < 0 &&
113                       myBuilder.fitsRightMargin(myAnonymousClass, document, getClosureStartOffset(), getClosureEndOffset(), header.length() + contents.length() + 5);
114   }
115
116   private int getClosureEndOffset() {
117     return myNewExpression.getTextRange().getEndOffset();
118   }
119
120   private int getClosureStartOffset() {
121     return myNewExpression.getTextRange().getStartOffset();
122   }
123
124   @Nullable
125   private List<NamedFoldingDescriptor> createDescriptors(PsiElement classRBrace,
126                                                          int rangeStart,
127                                                          int rangeEnd,
128                                                          String header,
129                                                          String footer) {
130     if (rangeStart >= rangeEnd) return null;
131
132     FoldingGroup group = FoldingGroup.newGroup("lambda");
133     List<NamedFoldingDescriptor> foldElements = new ArrayList<NamedFoldingDescriptor>();
134     foldElements.add(new NamedFoldingDescriptor(myNewExpression, getClosureStartOffset(), rangeStart, group, header));
135     if (rangeEnd + 1 < getClosureEndOffset()) {
136       foldElements.add(new NamedFoldingDescriptor(classRBrace, rangeEnd, getClosureEndOffset(), group, footer));
137     }
138     return foldElements;
139   }
140
141   @Nullable
142   private static String getClosureContents(int rangeStart, int rangeEnd, CharSequence seq) {
143     int firstLineStart = CharArrayUtil.shiftForward(seq, rangeStart, " \t");
144     if (firstLineStart < seq.length() - 1 && seq.charAt(firstLineStart) == '\n') firstLineStart++;
145
146     int lastLineEnd = CharArrayUtil.shiftBackward(seq, rangeEnd - 1, " \t");
147     if (lastLineEnd > 0 && seq.charAt(lastLineEnd) == '\n') lastLineEnd--;
148     if (lastLineEnd < firstLineStart) return null;
149     return seq.subSequence(firstLineStart, lastLineEnd).toString();
150   }
151
152   @NotNull
153   private String getFoldingHeader(String methodName) {
154     String type = myQuick ? "" : getOptionalLambdaType();
155     String params = StringUtil.join(myMethod.getParameterList().getParameters(), new Function<PsiParameter, String>() {
156       @Override
157       public String fun(PsiParameter psiParameter) {
158         return psiParameter.getName();
159       }
160     }, ", ");
161     return type + methodName + "(" + params + ") " + myBuilder.rightArrow() + " {";
162   }
163
164   @Nullable
165   static ClosureFolding prepare(PsiAnonymousClass anonymousClass, boolean quick, JavaFoldingBuilderBase builder) {
166     PsiElement parent = anonymousClass.getParent();
167     if (parent instanceof PsiNewExpression && hasNoArguments((PsiNewExpression)parent)) {
168       PsiClass baseClass = quick ? null : anonymousClass.getBaseClassType().resolve();
169       if (hasOnlyOneLambdaMethod(anonymousClass, !quick) && (quick || seemsLikeLambda(baseClass))) {
170         PsiMethod method = anonymousClass.getMethods()[0];
171         PsiCodeBlock body = method.getBody();
172         if (body != null) {
173           return new ClosureFolding(anonymousClass, (PsiNewExpression)parent, quick, baseClass, builder, method, body);
174         }
175       }
176     }
177     return null;
178   }
179
180   private static boolean hasNoArguments(PsiNewExpression expression) {
181     PsiExpressionList argumentList = expression.getArgumentList();
182     return argumentList != null && argumentList.getExpressions().length == 0;
183   }
184
185   private static boolean hasOnlyOneLambdaMethod(@NotNull PsiAnonymousClass anonymousClass, boolean checkResolve) {
186     PsiField[] fields = anonymousClass.getFields();
187     if (fields.length != 0) {
188       if (fields.length == 1 && HighlightUtilBase.SERIAL_VERSION_UID_FIELD_NAME.equals(fields[0].getName()) &&
189           fields[0].hasModifierProperty(PsiModifier.STATIC)) {
190         //ok
191       } else {
192         return false;
193       }
194     }
195     if (anonymousClass.getInitializers().length != 0 ||
196         anonymousClass.getInnerClasses().length != 0 ||
197         anonymousClass.getMethods().length != 1) {
198       return false;
199     }
200
201     PsiMethod method = anonymousClass.getMethods()[0];
202     if (method.hasModifierProperty(PsiModifier.SYNCHRONIZED)) {
203       return false;
204     }
205
206     if (checkResolve) {
207       for (PsiClassType type : method.getThrowsList().getReferencedTypes()) {
208         if (type.resolve() == null) {
209           return false;
210         }
211       }
212     }
213
214     return true;
215   }
216
217   static boolean seemsLikeLambda(@Nullable PsiClass baseClass) {
218     return baseClass != null && PsiUtil.hasDefaultConstructor(baseClass, true);
219   }
220
221   private String getOptionalLambdaType() {
222     if (myBuilder.shouldShowExplicitLambdaType(myAnonymousClass, myNewExpression)) {
223       String baseClassName = ObjectUtils.assertNotNull(myAnonymousClass.getBaseClassType().resolve()).getName();
224       if (baseClassName != null) {
225         return "(" + baseClassName + ") ";
226       }
227     }
228     return "";
229   }
230
231   private boolean shouldShowMethodName() {
232     if (myBaseClass == null || !myBaseClass.hasModifierProperty(PsiModifier.ABSTRACT)) return true;
233
234     for (PsiMethod method : myBaseClass.getMethods()) {
235       if (method.hasModifierProperty(PsiModifier.ABSTRACT)) {
236         return false;
237       }
238     }
239
240     try {
241       return OverrideImplementExploreUtil.getMethodSignaturesToImplement(myBaseClass).isEmpty();
242     }
243     catch (IndexNotReadyException e) {
244       return true;
245     }
246   }
247
248 }