2 * Copyright 2000-2014 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.folding.impl;
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;
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");
48 @NotNull private final Project myProject;
49 private final VirtualFile myFile;
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";
61 DocumentFoldingInfo(@NotNull Project project, @NotNull Document document) {
63 myFile = FileDocumentManager.getInstance().getFile(document);
66 void loadFromEditor(@NotNull Editor editor) {
67 assertDispatchThread();
68 LOG.assertTrue(!editor.isDisposed());
71 PsiDocumentManager documentManager = PsiDocumentManager.getInstance(myProject);
72 documentManager.commitDocument(editor.getDocument());
73 PsiFile file = documentManager.getPsiFile(editor.getDocument());
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);
90 else if (region.isValid()) {
91 myRangeMarkers.add(region);
92 region.putUserData(FOLDING_INFO_KEY, fi);
98 private static void assertDispatchThread() {
99 ApplicationManagerEx.getApplicationEx().assertIsDispatchThread();
102 void setToEditor(@NotNull final Editor editor) {
103 assertDispatchThread();
104 final PsiManager psiManager = PsiManager.getInstance(myProject);
105 if (psiManager.isDisposed()) return;
107 if (!myFile.isValid()) return;
108 final PsiFile psiFile = psiManager.findFile(myFile);
109 if (psiFile == null) return;
111 Map<PsiElement, FoldingDescriptor> ranges = null;
112 for (SmartPsiElementPointer<PsiElement> ptr: myPsiElements) {
113 PsiElement element = ptr.getElement();
114 if (element == null || !element.isValid()) {
118 if (ranges == null) {
119 ranges = buildRanges(editor, psiFile);
121 FoldingDescriptor descriptor = ranges.get(element);
122 if (descriptor == null) {
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);
134 for (RangeMarker marker : myRangeMarkers) {
135 if (!marker.isValid()) {
138 FoldRegion region = FoldingUtil.findFoldRegion(editor, marker.getStartOffset(), marker.getEndOffset());
139 FoldingInfo info = marker.getUserData(FOLDING_INFO_KEY);
140 if (region == null) {
142 region = editor.getFoldingModel().addFoldRegion(marker.getStartOffset(), marker.getEndOffset(), info.placeHolder);
144 if (region == null) {
149 boolean state = info != null && info.expanded;
150 region.setExpanded(state);
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();
165 ranges.put(psi, descriptor);
172 myPsiElements.clear();
173 for (RangeMarker marker : myRangeMarkers) {
174 if (!(marker instanceof FoldRegion)) marker.dispose();
176 myRangeMarkers.clear();
180 public void writeExternal(Element element) throws WriteExternalException {
181 PsiDocumentManager.getInstance(myProject).commitAllDocuments();
183 if (myPsiElements.isEmpty() && myRangeMarkers.isEmpty()){
184 throw new WriteExternalException();
187 for (SmartPsiElementPointer<PsiElement> ptr : myPsiElements) {
188 PsiElement psiElement = ptr.getElement();
189 if (psiElement == null || !psiElement.isValid()) {
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) {
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);
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);
214 for (RangeMarker marker : myRangeMarkers) {
215 FoldingInfo fi = marker.getUserData(FOLDING_INFO_KEY);
216 boolean state = fi != null && fi.expanded;
218 Element e = new Element(MARKER_TAG);
220 date = getTimeStamp();
222 if (date.isEmpty()) {
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);
237 public void readExternal(final Element element) {
238 ApplicationManager.getApplication().runReadAction(new Runnable() {
243 if (!myFile.isValid()) return;
245 final Document document = FileDocumentManager.getInstance().getDocument(myFile);
246 if (document == null) return;
248 PsiFile psiFile = PsiDocumentManager.getInstance(myProject).getPsiFile(document);
249 if (psiFile == null || !psiFile.getViewProvider().isPhysical()) return;
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) {
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);
267 else if (MARKER_TAG.equals(e.getName())) {
269 date = getTimeStamp();
271 if (date.isEmpty()) continue;
273 if (!date.equals(e.getAttributeValue(DATE_ATT)) || FileDocumentManager.getInstance().isDocumentUnsaved(document)) continue;
274 StringTokenizer tokenizer = new StringTokenizer(e.getAttributeValue(SIGNATURE_ATT), ":");
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);
286 catch (NoSuchElementException exc) {
291 throw new IllegalStateException("unknown tag: " + e.getName());
298 private String getTimeStamp() {
299 if (!myFile.isValid()) return "";
300 return Long.toString(myFile.getTimeStamp());
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();
313 public boolean equals(Object o) {
317 if (o == null || getClass() != o.getClass()) {
321 DocumentFoldingInfo info = (DocumentFoldingInfo)o;
323 if (myFile != null ? !myFile.equals(info.myFile) : info.myFile != null) {
326 if (!myProject.equals(info.myProject) || !myPsiElements.equals(info.myPsiElements)) {
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()) {
337 if (!TextRange.areSegmentsEqual(marker, other)) return false;
339 FoldingInfo fi = marker.getUserData(FOLDING_INFO_KEY);
340 FoldingInfo ofi = other.getUserData(FOLDING_INFO_KEY);
341 if (!Comparing.equal(fi, ofi)) return false;
346 private static class FoldingInfo {
347 private final String placeHolder;
348 private final boolean expanded;
350 private FoldingInfo(@NotNull String placeHolder, boolean expanded) {
351 this.placeHolder = placeHolder;
352 this.expanded = expanded;
356 public boolean equals(Object o) {
360 if (o == null || getClass() != o.getClass()) {
364 FoldingInfo info = (FoldingInfo)o;
366 return expanded == info.expanded && placeHolder.equals(info.placeHolder);
370 public int hashCode() {
371 int result = placeHolder.hashCode();
372 result = 31 * result + (expanded ? 1 : 0);