fold as closures anonymous classes with a single implemented method, as long they...
[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 header = getFoldingHeader();
79     if (showSingleLineFolding(document, contents, header)) {
80       return createDescriptors(classRBrace, trimStartSpaces(seq, rangeStart), trimTailSpaces(seq, rangeEnd), header + " ", " }");
81     }
82
83     return createDescriptors(classRBrace, rangeStart, rangeEnd, header, "}");
84   }
85
86   private static int trimStartSpaces(CharSequence seq, int rangeStart) {
87     return CharArrayUtil.shiftForward(seq, rangeStart, " \n\t");
88   }
89
90   private static int trimTailSpaces(CharSequence seq, int rangeEnd) {
91     return CharArrayUtil.shiftBackward(seq, rangeEnd - 1, " \n\t") + 1;
92   }
93
94   private static int getContentRangeEnd(Document document, PsiJavaToken rbrace, PsiElement classRBrace) {
95     CharSequence seq = document.getCharsSequence();
96     int rangeEnd = rbrace.getTextRange().getStartOffset();
97
98     int methodEndLine = document.getLineNumber(rangeEnd);
99     int methodEndLineStart = document.getLineStartOffset(methodEndLine);
100     if ("}".equals(seq.subSequence(methodEndLineStart, document.getLineEndOffset(methodEndLine)).toString().trim())) {
101       int classEndStart = classRBrace.getTextRange().getStartOffset();
102       int classEndCol = classEndStart - document.getLineStartOffset(document.getLineNumber(classEndStart));
103       return classEndCol + methodEndLineStart;
104     }
105     return rangeEnd;
106   }
107
108   private boolean showSingleLineFolding(Document document, String contents, String header) {
109     return contents.indexOf('\n') < 0 &&
110                       myBuilder.fitsRightMargin(myAnonymousClass, document, getClosureStartOffset(), getClosureEndOffset(), header.length() + contents.length() + 5);
111   }
112
113   private int getClosureEndOffset() {
114     return myNewExpression.getTextRange().getEndOffset();
115   }
116
117   private int getClosureStartOffset() {
118     return myNewExpression.getTextRange().getStartOffset();
119   }
120
121   @Nullable
122   private List<NamedFoldingDescriptor> createDescriptors(PsiElement classRBrace,
123                                                          int rangeStart,
124                                                          int rangeEnd,
125                                                          String header,
126                                                          String footer) {
127     if (rangeStart >= rangeEnd) return null;
128
129     FoldingGroup group = FoldingGroup.newGroup("lambda");
130     List<NamedFoldingDescriptor> foldElements = new ArrayList<NamedFoldingDescriptor>();
131     foldElements.add(new NamedFoldingDescriptor(myNewExpression, getClosureStartOffset(), rangeStart, group, header));
132     if (rangeEnd + 1 < getClosureEndOffset()) {
133       foldElements.add(new NamedFoldingDescriptor(classRBrace, rangeEnd, getClosureEndOffset(), group, footer));
134     }
135     return foldElements;
136   }
137
138   @Nullable
139   private static String getClosureContents(int rangeStart, int rangeEnd, CharSequence seq) {
140     int firstLineStart = CharArrayUtil.shiftForward(seq, rangeStart, " \t");
141     if (firstLineStart < seq.length() - 1 && seq.charAt(firstLineStart) == '\n') firstLineStart++;
142
143     int lastLineEnd = CharArrayUtil.shiftBackward(seq, rangeEnd - 1, " \t");
144     if (lastLineEnd > 0 && seq.charAt(lastLineEnd) == '\n') lastLineEnd--;
145     if (lastLineEnd < firstLineStart) return null;
146     return seq.subSequence(firstLineStart, lastLineEnd).toString();
147   }
148
149   private String getFoldingHeader() {
150     String methodName = shouldShowMethodName() ? myMethod.getName() : "";
151     String type = myQuick ? "" : getOptionalLambdaType();
152     String params = StringUtil.join(myMethod.getParameterList().getParameters(), new Function<PsiParameter, String>() {
153       @Override
154       public String fun(PsiParameter psiParameter) {
155         return psiParameter.getName();
156       }
157     }, ", ");
158     return type + methodName + "(" + params + ") " + myBuilder.rightArrow() + " {";
159   }
160
161   @Nullable
162   static ClosureFolding prepare(PsiAnonymousClass anonymousClass, boolean quick, JavaFoldingBuilderBase builder) {
163     PsiElement parent = anonymousClass.getParent();
164     if (parent instanceof PsiNewExpression && hasNoArguments((PsiNewExpression)parent)) {
165       PsiClass baseClass = quick ? null : anonymousClass.getBaseClassType().resolve();
166       if (hasOnlyOneLambdaMethod(anonymousClass, !quick) && (quick || seemsLikeLambda(baseClass, anonymousClass))) {
167         PsiMethod method = anonymousClass.getMethods()[0];
168         PsiCodeBlock body = method.getBody();
169         if (body != null) {
170           return new ClosureFolding(anonymousClass, (PsiNewExpression)parent, quick, baseClass, builder, method, body);
171         }
172       }
173     }
174     return null;
175   }
176
177   private static boolean hasNoArguments(PsiNewExpression expression) {
178     PsiExpressionList argumentList = expression.getArgumentList();
179     return argumentList != null && argumentList.getExpressions().length == 0;
180   }
181
182   private static boolean hasOnlyOneLambdaMethod(@NotNull PsiAnonymousClass anonymousClass, boolean checkResolve) {
183     PsiField[] fields = anonymousClass.getFields();
184     if (fields.length != 0) {
185       if (fields.length == 1 && HighlightUtilBase.SERIAL_VERSION_UID_FIELD_NAME.equals(fields[0].getName()) &&
186           fields[0].hasModifierProperty(PsiModifier.STATIC)) {
187         //ok
188       } else {
189         return false;
190       }
191     }
192     if (anonymousClass.getInitializers().length != 0 ||
193         anonymousClass.getInnerClasses().length != 0 ||
194         anonymousClass.getMethods().length != 1) {
195       return false;
196     }
197
198     PsiMethod method = anonymousClass.getMethods()[0];
199     if (method.hasModifierProperty(PsiModifier.SYNCHRONIZED)) {
200       return false;
201     }
202
203     if (checkResolve) {
204       for (PsiClassType type : method.getThrowsList().getReferencedTypes()) {
205         if (type.resolve() == null) {
206           return false;
207         }
208       }
209     }
210
211     return true;
212   }
213
214   static boolean seemsLikeLambda(@Nullable PsiClass baseClass, @NotNull PsiElement context) {
215     if (baseClass == null || !PsiUtil.hasDefaultConstructor(baseClass, true)) return false;
216
217     if (PsiUtil.isLanguageLevel8OrHigher(context) && LambdaUtil.isFunctionalClass(baseClass)) {
218       return false;
219     }
220
221     return true;
222   }
223
224   private String getOptionalLambdaType() {
225     if (myBuilder.shouldShowExplicitLambdaType(myAnonymousClass, myNewExpression)) {
226       String baseClassName = ObjectUtils.assertNotNull(myAnonymousClass.getBaseClassType().resolve()).getName();
227       if (baseClassName != null) {
228         return "(" + baseClassName + ") ";
229       }
230     }
231     return "";
232   }
233
234   private boolean shouldShowMethodName() {
235     if (myBaseClass == null || !myBaseClass.hasModifierProperty(PsiModifier.ABSTRACT)) return true;
236
237     for (PsiMethod method : myBaseClass.getMethods()) {
238       if (method.hasModifierProperty(PsiModifier.ABSTRACT)) {
239         return false;
240       }
241     }
242
243     try {
244       return OverrideImplementExploreUtil.getMethodSignaturesToImplement(myBaseClass).isEmpty();
245     }
246     catch (IndexNotReadyException e) {
247       return true;
248     }
249   }
250
251 }