IG: recognize javax.annotation.CheckReturnValue (IDEA-19564)
[idea/community.git] / platform / lang-impl / src / com / intellij / lang / folding / CustomFoldingSurroundDescriptor.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 package com.intellij.lang.folding;
17
18 import com.intellij.lang.ASTNode;
19 import com.intellij.lang.Commenter;
20 import com.intellij.lang.Language;
21 import com.intellij.lang.LanguageCommenters;
22 import com.intellij.lang.parser.GeneratedParserUtilBase;
23 import com.intellij.lang.surroundWith.SurroundDescriptor;
24 import com.intellij.lang.surroundWith.Surrounder;
25 import com.intellij.openapi.editor.Document;
26 import com.intellij.openapi.editor.Editor;
27 import com.intellij.openapi.editor.RangeMarker;
28 import com.intellij.openapi.project.Project;
29 import com.intellij.openapi.util.TextRange;
30 import com.intellij.openapi.util.text.StringUtil;
31 import com.intellij.psi.*;
32 import com.intellij.psi.codeStyle.CodeStyleManager;
33 import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
34 import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
35 import com.intellij.psi.util.PsiTreeUtil;
36 import com.intellij.util.IncorrectOperationException;
37 import org.jetbrains.annotations.NotNull;
38 import org.jetbrains.annotations.Nullable;
39
40 import java.util.ArrayList;
41 import java.util.List;
42
43 import static com.intellij.psi.util.PsiTreeUtil.skipParentsOfType;
44
45 /**
46  * @author Rustam Vishnyakov
47  */
48 public class CustomFoldingSurroundDescriptor implements SurroundDescriptor {
49
50   public final static CustomFoldingSurroundDescriptor INSTANCE = new CustomFoldingSurroundDescriptor();
51   public final static CustomFoldingRegionSurrounder[] SURROUNDERS;
52
53   private final static String DEFAULT_DESC_TEXT = "Description";
54
55   static {
56     List<CustomFoldingRegionSurrounder> surrounderList = new ArrayList<CustomFoldingRegionSurrounder>();
57     for (CustomFoldingProvider provider : CustomFoldingProvider.getAllProviders()) {
58       surrounderList.add(new CustomFoldingRegionSurrounder(provider));
59     }
60     SURROUNDERS = surrounderList.toArray(new CustomFoldingRegionSurrounder[surrounderList.size()]);
61   }
62
63   @NotNull
64   @Override
65   public PsiElement[] getElementsToSurround(PsiFile file, int startOffset, int endOffset) {
66     if (startOffset >= endOffset) return PsiElement.EMPTY_ARRAY;
67     Commenter commenter = LanguageCommenters.INSTANCE.forLanguage(file.getLanguage());
68     if (commenter == null || commenter.getLineCommentPrefix() == null) return PsiElement.EMPTY_ARRAY;
69     PsiElement startElement = file.findElementAt(startOffset);
70     if (startElement instanceof PsiWhiteSpace) startElement = startElement.getNextSibling();
71     PsiElement endElement = file.findElementAt(endOffset - 1);
72     if (endElement instanceof PsiWhiteSpace) endElement = endElement.getPrevSibling();
73     if (startElement != null && endElement != null) {
74       startElement = findClosestParentAfterLineBreak(startElement);
75       if (startElement != null) {
76         endElement = findClosestParentBeforeLineBreak(endElement);
77         if (endElement != null) {
78           return adjustRange(startElement, endElement);
79         }
80       }
81     }
82     return PsiElement.EMPTY_ARRAY;
83   }
84
85   @NotNull
86   private static PsiElement[] adjustRange(@NotNull PsiElement start, @NotNull PsiElement end) {
87     PsiElement newStart = lowerStartElementIfNeeded(start, end);
88     PsiElement newEnd = lowerEndElementIfNeeded(start, end);
89     if (newStart == null || newEnd == null) {
90       return PsiElement.EMPTY_ARRAY;
91     }
92     PsiElement commonParent = findCommonAncestorForWholeRange(newStart, newEnd);
93     if (commonParent != null) {
94       return new PsiElement[] {commonParent};
95     }
96     // If either start or end element is the first/last leaf element in its parent, use the parent itself instead
97     // to prevent selection of clearly illegal ranges like the following:
98     // [
99     //   <selection>1
100     // ]</selection>
101     // E.g. in case shown, because of that adjustment, closing bracket and number literal won't have the same parent
102     // and next test will fail.
103     PsiElement newStartParent = getParent(newStart);
104     if (newStartParent != null && newStartParent.getFirstChild() == newStart && newStart.getFirstChild() == null) {
105       newStart = newStartParent;
106     }
107     PsiElement newEndParent = getParent(newEnd);
108     if (newEndParent != null && newEndParent.getLastChild() == newEnd && newEnd.getFirstChild() == null) {
109       newEnd = newEndParent;
110     }
111     if (getParent(newStart) == getParent(newEnd)) {
112       return new PsiElement[] {newStart, newEnd};
113     }
114     return PsiElement.EMPTY_ARRAY;
115   }
116
117   @Nullable
118   private static PsiElement getParent(@Nullable PsiElement e) {
119     return e instanceof PsiFile ? e : skipParentsOfType(e, GeneratedParserUtilBase.DummyBlock.class);
120   }
121
122   @Nullable
123   private static PsiElement lowerEndElementIfNeeded(@NotNull PsiElement start, @NotNull PsiElement end) {
124     if (PsiTreeUtil.isAncestor(end, start, true)) {
125       PsiElement o = end.getLastChild();
126       while (o != null && o.getParent() != start.getParent()) {
127         PsiElement last = o.getLastChild();
128         if (last == null) return o;
129         o = last;
130       }
131       return o;
132     }
133     return end;
134   }
135
136   @Nullable
137   private static PsiElement lowerStartElementIfNeeded(@NotNull PsiElement start, @NotNull PsiElement end) {
138     if (PsiTreeUtil.isAncestor(start, end, true)) {
139       PsiElement o = start.getFirstChild();
140       while (o != null && o.getParent() != end.getParent()) {
141         PsiElement first = o.getFirstChild();
142         if (first == null) return o;
143         o = first;
144       }
145       return o;
146     }
147     return start;
148   }
149
150   @Nullable
151   private static PsiElement findCommonAncestorForWholeRange(@NotNull PsiElement start, @NotNull PsiElement end) {
152     if (start.getContainingFile() != end.getContainingFile()) {
153       return null;
154     }
155     final PsiElement parent = PsiTreeUtil.findCommonParent(start, end);
156     if (parent == null) {
157       return null;
158     }
159     final TextRange parentRange = parent.getTextRange();
160     if (parentRange.getStartOffset() == start.getTextRange().getStartOffset() &&
161         parentRange.getEndOffset() == end.getTextRange().getEndOffset()) {
162       return parent;
163     }
164     return null;
165   }
166
167   @Nullable
168   private static PsiElement findClosestParentAfterLineBreak(PsiElement element) {
169     PsiElement parent = element;
170     while (parent != null && !(parent instanceof PsiFileSystemItem)) {
171       PsiElement prev = parent.getPrevSibling();
172       while (prev != null && prev.getTextLength() <= 0) {
173         prev = prev.getPrevSibling();
174       }
175       if (firstElementInFile(parent)) {
176         return parent.getContainingFile();
177       }
178       else if (isWhiteSpaceWithLineFeed(prev)) {
179         return parent;
180       }
181       parent = parent.getParent();
182     }
183     return null;
184   }
185
186   private static boolean firstElementInFile(@NotNull PsiElement element) {
187     return element.getTextOffset() == 0;
188   }
189
190   @Nullable
191   private static PsiElement findClosestParentBeforeLineBreak(PsiElement element) {
192     PsiElement parent = element;
193     while (parent != null && !(parent instanceof PsiFileSystemItem)) {
194       final PsiElement next = parent.getNextSibling();
195       if (lastElementInFile(parent)) {
196         return parent.getContainingFile();
197       }
198       else if (isWhiteSpaceWithLineFeed(next)) {
199         return parent;
200       }
201       parent = parent.getParent();
202     }
203     return null;
204   }
205
206   private static boolean lastElementInFile(@NotNull PsiElement element) {
207     return element.getTextRange().getEndOffset() == element.getContainingFile().getTextRange().getEndOffset();
208   }
209
210   private static boolean isWhiteSpaceWithLineFeed(@Nullable PsiElement element) {
211     if (element == null) {
212       return false;
213     }
214     if (element instanceof PsiWhiteSpace) {
215       return element.textContains('\n');
216     }
217     final ASTNode node = element.getNode();
218     if (node == null) {
219       return false;
220     }
221     final CharSequence text = node.getChars();
222     boolean lineFeedFound = false;
223     for (int i = 0; i < text.length(); i++) {
224       final char c = text.charAt(i);
225       if (!StringUtil.isWhiteSpace(c)) {
226         return false;
227       }
228       lineFeedFound |= c == '\n';
229     }
230     return lineFeedFound;
231   }
232
233   @NotNull
234   @Override
235   public Surrounder[] getSurrounders() {
236     return SURROUNDERS;
237   }
238
239   @Override
240   public boolean isExclusive() {
241     return false;
242   }
243
244   private static class CustomFoldingRegionSurrounder implements Surrounder {
245
246     private final CustomFoldingProvider myProvider;
247
248     public CustomFoldingRegionSurrounder(@NotNull CustomFoldingProvider provider) {
249       myProvider = provider;
250     }
251
252     @Override
253     public String getTemplateDescription() {
254       return myProvider.getDescription();
255     }
256
257     @Override
258     public boolean isApplicable(@NotNull PsiElement[] elements) {
259       if (elements.length == 0) return false;
260       if (elements[0].getContainingFile() instanceof PsiCodeFragment) {
261         return false;
262       }
263       for (FoldingBuilder each : LanguageFolding.INSTANCE.allForLanguage(elements[0].getLanguage())) {
264         if (each instanceof CustomFoldingBuilder) return true;
265       }
266       return false;
267     }
268
269     @Override
270     public TextRange surroundElements(@NotNull Project project, @NotNull Editor editor, @NotNull PsiElement[] elements)
271       throws IncorrectOperationException {
272       if (elements.length == 0) return null;
273       PsiElement firstElement = elements[0];
274       PsiElement lastElement = elements[elements.length - 1];
275       PsiFile psiFile = firstElement.getContainingFile();
276       Language language = psiFile.getLanguage();
277       Commenter commenter = LanguageCommenters.INSTANCE.forLanguage(language);
278       if (commenter == null) return null;
279       String linePrefix = commenter.getLineCommentPrefix();
280       if (linePrefix == null) return null;
281       int prefixLength = linePrefix.length();
282       int startOffset = firstElement.getTextRange().getStartOffset();
283       final Document document = editor.getDocument();
284       final int startLineNumber = document.getLineNumber(startOffset);
285       final String startIndent = document.getText(new TextRange(document.getLineStartOffset(startLineNumber), startOffset));
286       int endOffset = lastElement.getTextRange().getEndOffset();
287       int delta = 0;
288       TextRange rangeToSelect = TextRange.create(startOffset, startOffset);
289       String startText = myProvider.getStartString();
290       int descPos = startText.indexOf("?");
291       if (descPos >= 0) {
292         startText = startText.replace("?", DEFAULT_DESC_TEXT);
293         rangeToSelect = TextRange.from(startOffset + descPos, DEFAULT_DESC_TEXT.length());
294       }
295
296       String startString = linePrefix + startText + "\n" + startIndent;
297       String endString = "\n" + linePrefix + myProvider.getEndString();
298       document.insertString(endOffset, endString);
299       delta += endString.length();
300       document.insertString(startOffset, startString);
301       delta += startString.length();
302       
303       RangeMarker rangeMarkerToSelect = document.createRangeMarker(rangeToSelect.shiftRight(prefixLength));
304       PsiDocumentManager.getInstance(project).commitDocument(document);
305       adjustLineIndent(project, psiFile, language, TextRange.from(endOffset + delta - endString.length(), endString.length()));
306       adjustLineIndent(project, psiFile, language, TextRange.from(startOffset, startString.length()));
307       rangeToSelect = TextRange.create(rangeMarkerToSelect.getStartOffset(), rangeMarkerToSelect.getEndOffset());
308       rangeMarkerToSelect.dispose();
309       return rangeToSelect;
310     }
311
312     private static void adjustLineIndent(@NotNull Project project, PsiFile file, Language language, TextRange range) {
313       CommonCodeStyleSettings formatSettings = CodeStyleSettingsManager.getSettings(project).getCommonSettings(language);
314       boolean keepAtFirstCol = formatSettings.KEEP_FIRST_COLUMN_COMMENT;
315       formatSettings.KEEP_FIRST_COLUMN_COMMENT = false;
316       CodeStyleManager.getInstance(project).adjustLineIndent(file, range);
317       formatSettings.KEEP_FIRST_COLUMN_COMMENT = keepAtFirstCol;
318     }
319   }
320 }