27ad0ba8118b295204b270c16e8330ea315e04db
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / folding / impl / DocumentFoldingInfo.java
1 /*
2  * Copyright 2000-2014 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.folding.impl;
18
19 import com.intellij.lang.ASTNode;
20 import com.intellij.lang.folding.FoldingBuilder;
21 import com.intellij.lang.folding.FoldingDescriptor;
22 import com.intellij.lang.folding.LanguageFolding;
23 import com.intellij.openapi.application.ApplicationManager;
24 import com.intellij.openapi.application.ex.ApplicationManagerEx;
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.FoldRegion;
29 import com.intellij.openapi.editor.RangeMarker;
30 import com.intellij.openapi.fileEditor.FileDocumentManager;
31 import com.intellij.openapi.fileEditor.impl.text.CodeFoldingState;
32 import com.intellij.openapi.project.Project;
33 import com.intellij.openapi.util.*;
34 import com.intellij.openapi.vfs.VirtualFile;
35 import com.intellij.psi.*;
36 import com.intellij.util.containers.ContainerUtil;
37 import com.intellij.util.text.StringTokenizer;
38 import org.jdom.Element;
39 import org.jetbrains.annotations.NonNls;
40 import org.jetbrains.annotations.NotNull;
41
42 import java.util.*;
43
44 class DocumentFoldingInfo implements JDOMExternalizable, CodeFoldingState {
45   private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.folding.impl.DocumentFoldingInfo");
46   private static final Key<FoldingInfo> FOLDING_INFO_KEY = Key.create("FOLDING_INFO");
47
48   @NotNull private final Project myProject;
49   private final VirtualFile myFile;
50
51   @NotNull private final List<SmartPsiElementPointer<PsiElement>> myPsiElements = ContainerUtil.createLockFreeCopyOnWriteList();
52   @NotNull private final List<RangeMarker> myRangeMarkers = ContainerUtil.createLockFreeCopyOnWriteList();
53   private static final String DEFAULT_PLACEHOLDER = "...";
54   @NonNls private static final String ELEMENT_TAG = "element";
55   @NonNls private static final String SIGNATURE_ATT = "signature";
56   @NonNls private static final String EXPANDED_ATT = "expanded";
57   @NonNls private static final String MARKER_TAG = "marker";
58   @NonNls private static final String DATE_ATT = "date";
59   @NonNls private static final String PLACEHOLDER_ATT = "placeholder";
60
61   DocumentFoldingInfo(@NotNull Project project, @NotNull Document document) {
62     myProject = project;
63     myFile = FileDocumentManager.getInstance().getFile(document);
64   }
65
66   void loadFromEditor(@NotNull Editor editor) {
67     assertDispatchThread();
68     LOG.assertTrue(!editor.isDisposed());
69     clear();
70
71     PsiDocumentManager documentManager = PsiDocumentManager.getInstance(myProject);
72     documentManager.commitDocument(editor.getDocument());
73     PsiFile file = documentManager.getPsiFile(editor.getDocument());
74
75     SmartPointerManager smartPointerManager = SmartPointerManager.getInstance(myProject);
76     EditorFoldingInfo info = EditorFoldingInfo.get(editor);
77     FoldRegion[] foldRegions = editor.getFoldingModel().getAllFoldRegions();
78     for (FoldRegion region : foldRegions) {
79       PsiElement element = info.getPsiElement(region);
80       boolean expanded = region.isExpanded();
81       boolean collapseByDefault = element != null &&
82                                   FoldingPolicy.isCollapseByDefault(element) &&
83                                   !FoldingUtil.caretInsideRange(editor, TextRange.create(region));
84       if (collapseByDefault == expanded || element == null) {
85         FoldingInfo fi = new FoldingInfo(region.getPlaceholderText(), expanded);
86         if (element != null) {
87           myPsiElements.add(smartPointerManager.createSmartPsiElementPointer(element, file));
88           element.putUserData(FOLDING_INFO_KEY, fi);
89         }
90         else if (region.isValid()) {
91           myRangeMarkers.add(region);
92           region.putUserData(FOLDING_INFO_KEY, fi);
93         }
94       }
95     }
96   }
97
98   private static void assertDispatchThread() {
99     ApplicationManagerEx.getApplicationEx().assertIsDispatchThread();
100   }
101
102   void setToEditor(@NotNull final Editor editor) {
103     assertDispatchThread();
104     final PsiManager psiManager = PsiManager.getInstance(myProject);
105     if (psiManager.isDisposed()) return;
106
107     if (!myFile.isValid()) return;
108     final PsiFile psiFile = psiManager.findFile(myFile);
109     if (psiFile == null) return;
110
111     Map<PsiElement, FoldingDescriptor> ranges = null;
112     for (SmartPsiElementPointer<PsiElement> ptr: myPsiElements) {
113       PsiElement element = ptr.getElement();
114       if (element == null || !element.isValid()) {
115         continue;
116       }
117
118       if (ranges == null) {
119         ranges = buildRanges(editor, psiFile);
120       }
121       FoldingDescriptor descriptor = ranges.get(element);
122       if (descriptor == null) {
123         continue;
124       }
125
126       TextRange range = descriptor.getRange();
127       FoldRegion region = FoldingUtil.findFoldRegion(editor, range.getStartOffset(), range.getEndOffset());
128       if (region != null) {
129         FoldingInfo fi = element.getUserData(FOLDING_INFO_KEY);
130         boolean state = fi != null && fi.expanded;
131         region.setExpanded(state);
132       }
133     }
134     for (RangeMarker marker : myRangeMarkers) {
135       if (!marker.isValid()) {
136         continue;
137       }
138       FoldRegion region = FoldingUtil.findFoldRegion(editor, marker.getStartOffset(), marker.getEndOffset());
139       FoldingInfo info = marker.getUserData(FOLDING_INFO_KEY);
140       if (region == null) {
141         if (info != null) {
142           region = editor.getFoldingModel().addFoldRegion(marker.getStartOffset(), marker.getEndOffset(), info.placeHolder);
143         }
144         if (region == null) {
145           return;
146         }
147       }
148
149       boolean state = info != null && info.expanded;
150       region.setExpanded(state);
151     }
152   }
153
154   @NotNull
155   private static Map<PsiElement, FoldingDescriptor> buildRanges(@NotNull Editor editor, @NotNull PsiFile psiFile) {
156     final FoldingBuilder foldingBuilder = LanguageFolding.INSTANCE.forLanguage(psiFile.getLanguage());
157     final ASTNode node = psiFile.getNode();
158     if (node == null) return Collections.emptyMap();
159     final FoldingDescriptor[] descriptors = LanguageFolding.buildFoldingDescriptors(foldingBuilder, psiFile, editor.getDocument(), true);
160     Map<PsiElement, FoldingDescriptor> ranges = new HashMap<PsiElement, FoldingDescriptor>();
161     for (FoldingDescriptor descriptor : descriptors) {
162       final ASTNode ast = descriptor.getElement();
163       final PsiElement psi = ast.getPsi();
164       if (psi != null) {
165         ranges.put(psi, descriptor);
166       }
167     }
168     return ranges;
169   }
170
171   void clear() {
172     myPsiElements.clear();
173     for (RangeMarker marker : myRangeMarkers) {
174       if (!(marker instanceof FoldRegion)) marker.dispose();
175     }
176     myRangeMarkers.clear();
177   }
178
179   @Override
180   public void writeExternal(Element element) throws WriteExternalException {
181     PsiDocumentManager.getInstance(myProject).commitAllDocuments();
182
183     if (myPsiElements.isEmpty() && myRangeMarkers.isEmpty()){
184       throw new WriteExternalException();
185     }
186
187     for (SmartPsiElementPointer<PsiElement> ptr : myPsiElements) {
188       PsiElement psiElement = ptr.getElement();
189       if (psiElement == null || !psiElement.isValid()) {
190         continue;
191       }
192       FoldingInfo fi = psiElement.getUserData(FOLDING_INFO_KEY);
193       boolean state = fi != null && fi.expanded;
194       String signature = FoldingPolicy.getSignature(psiElement);
195       if (signature == null) {
196         continue;
197       }
198
199       PsiElement restoredElement = FoldingPolicy.restoreBySignature(psiElement.getContainingFile(), signature);
200       if (!psiElement.equals(restoredElement)) {
201         StringBuilder trace = new StringBuilder();
202         PsiElement restoredAgain = FoldingPolicy.restoreBySignature(psiElement.getContainingFile(), signature, trace);
203         LOG.error("element: " + psiElement + "(" + psiElement.getText() + "); restoredElement: " + restoredElement
204                   + "; signature: '" + signature + "'; file: " + psiElement.getContainingFile() + "; restored again: "
205                   + restoredAgain + "; restore produces same results: " + (restoredAgain == restoredElement) + "; trace:\n" + trace);
206       }
207
208       Element e = new Element(ELEMENT_TAG);
209       e.setAttribute(SIGNATURE_ATT, signature);
210       e.setAttribute(EXPANDED_ATT, Boolean.toString(state));
211       element.addContent(e);
212     }
213     String date = null;
214     for (RangeMarker marker : myRangeMarkers) {
215       FoldingInfo fi = marker.getUserData(FOLDING_INFO_KEY);
216       boolean state = fi != null && fi.expanded;
217
218       Element e = new Element(MARKER_TAG);
219       if (date == null) {
220         date = getTimeStamp();
221       }
222       if (date.isEmpty()) {
223         continue;
224       }
225
226       e.setAttribute(DATE_ATT, date);
227       e.setAttribute(EXPANDED_ATT, Boolean.toString(state));
228       String signature = Integer.valueOf(marker.getStartOffset()) + ":" + Integer.valueOf(marker.getEndOffset());
229       e.setAttribute(SIGNATURE_ATT, signature);
230       String placeHolderText = fi == null ? DEFAULT_PLACEHOLDER : fi.placeHolder;
231       e.setAttribute(PLACEHOLDER_ATT, placeHolderText);
232       element.addContent(e);
233     }
234   }
235
236   @Override
237   public void readExternal(final Element element) {
238     ApplicationManager.getApplication().runReadAction(new Runnable() {
239       @Override
240       public void run() {
241         clear();
242
243         if (!myFile.isValid()) return;
244
245         final Document document = FileDocumentManager.getInstance().getDocument(myFile);
246         if (document == null) return;
247
248         PsiFile psiFile = PsiDocumentManager.getInstance(myProject).getPsiFile(document);
249         if (psiFile == null || !psiFile.getViewProvider().isPhysical()) return;
250
251         String date = null;
252         for (final Object o : element.getChildren()) {
253           Element e = (Element)o;
254           Boolean expanded = Boolean.valueOf(e.getAttributeValue(EXPANDED_ATT));
255           if (ELEMENT_TAG.equals(e.getName())) {
256             String signature = e.getAttributeValue(SIGNATURE_ATT);
257             if (signature == null) {
258               continue;
259             }
260             PsiElement restoredElement = FoldingPolicy.restoreBySignature(psiFile, signature);
261             if (restoredElement != null && restoredElement.isValid()) {
262               myPsiElements.add(SmartPointerManager.getInstance(myProject).createSmartPsiElementPointer(restoredElement));
263               FoldingInfo fi = new FoldingInfo(DEFAULT_PLACEHOLDER, expanded);
264               restoredElement.putUserData(FOLDING_INFO_KEY, fi);
265             }
266           }
267           else if (MARKER_TAG.equals(e.getName())) {
268             if (date == null) {
269               date = getTimeStamp();
270             }
271             if (date.isEmpty()) continue;
272
273             if (!date.equals(e.getAttributeValue(DATE_ATT)) || FileDocumentManager.getInstance().isDocumentUnsaved(document)) continue;
274             StringTokenizer tokenizer = new StringTokenizer(e.getAttributeValue(SIGNATURE_ATT), ":");
275             try {
276               int start = Integer.valueOf(tokenizer.nextToken()).intValue();
277               int end = Integer.valueOf(tokenizer.nextToken()).intValue();
278               if (start < 0 || end >= document.getTextLength() || start > end) continue;
279               RangeMarker marker = document.createRangeMarker(start, end);
280               myRangeMarkers.add(marker);
281               String placeHolderText = e.getAttributeValue(PLACEHOLDER_ATT);
282               if (placeHolderText == null) placeHolderText = DEFAULT_PLACEHOLDER;
283               FoldingInfo fi = new FoldingInfo(placeHolderText, expanded);
284               marker.putUserData(FOLDING_INFO_KEY, fi);
285             }
286             catch (NoSuchElementException exc) {
287               LOG.error(exc);
288             }
289           }
290           else {
291             throw new IllegalStateException("unknown tag: " + e.getName());
292           }
293         }
294       }
295     });
296   }
297
298   private String getTimeStamp() {
299     if (!myFile.isValid()) return "";
300     return Long.toString(myFile.getTimeStamp());
301   }
302
303   @Override
304   public int hashCode() {
305     int result = myProject.hashCode();
306     result = 31 * result + (myFile != null ? myFile.hashCode() : 0);
307     result = 31 * result + myPsiElements.hashCode();
308     result = 31 * result + myRangeMarkers.hashCode();
309     return result;
310   }
311
312   @Override
313   public boolean equals(Object o) {
314     if (this == o) {
315       return true;
316     }
317     if (o == null || getClass() != o.getClass()) {
318       return false;
319     }
320
321     DocumentFoldingInfo info = (DocumentFoldingInfo)o;
322
323     if (myFile != null ? !myFile.equals(info.myFile) : info.myFile != null) {
324       return false;
325     }
326     if (!myProject.equals(info.myProject) || !myPsiElements.equals(info.myPsiElements)) {
327       return false;
328     }
329
330     if (myRangeMarkers.size() != info.myRangeMarkers.size()) return false;
331     for (int i = 0; i < myRangeMarkers.size(); i++) {
332       RangeMarker marker = myRangeMarkers.get(i);
333       RangeMarker other = info.myRangeMarkers.get(i);
334       if (marker == other || !marker.isValid() || !other.isValid()) {
335         continue;
336       }
337       if (!TextRange.areSegmentsEqual(marker, other)) return false;
338
339       FoldingInfo fi = marker.getUserData(FOLDING_INFO_KEY);
340       FoldingInfo ofi = other.getUserData(FOLDING_INFO_KEY);
341       if (!Comparing.equal(fi, ofi)) return false;
342     }
343     return true;
344   }
345
346   private static class FoldingInfo {
347     private final String placeHolder;
348     private final boolean expanded;
349
350     private FoldingInfo(@NotNull String placeHolder, boolean expanded) {
351       this.placeHolder = placeHolder;
352       this.expanded = expanded;
353     }
354
355     @Override
356     public boolean equals(Object o) {
357       if (this == o) {
358         return true;
359       }
360       if (o == null || getClass() != o.getClass()) {
361         return false;
362       }
363
364       FoldingInfo info = (FoldingInfo)o;
365
366       return expanded == info.expanded && placeHolder.equals(info.placeHolder);
367     }
368
369     @Override
370     public int hashCode() {
371       int result = placeHolder.hashCode();
372       result = 31 * result + (expanded ? 1 : 0);
373       return result;
374     }
375   }
376 }