OC-9609 broken based on AST-tree folding at project opening +review CR-OC-1561
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / folding / impl / CodeFoldingManagerImpl.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.codeInsight.folding.CodeFoldingManager;
20 import com.intellij.codeInsight.hint.EditorFragmentComponent;
21 import com.intellij.codeInsight.hint.HintManager;
22 import com.intellij.openapi.application.ApplicationManager;
23 import com.intellij.openapi.application.ex.ApplicationManagerEx;
24 import com.intellij.openapi.components.ProjectComponent;
25 import com.intellij.openapi.editor.*;
26 import com.intellij.openapi.editor.event.EditorMouseEvent;
27 import com.intellij.openapi.editor.event.EditorMouseEventArea;
28 import com.intellij.openapi.editor.event.EditorMouseMotionAdapter;
29 import com.intellij.openapi.editor.ex.DocumentBulkUpdateListener;
30 import com.intellij.openapi.editor.ex.EditorEx;
31 import com.intellij.openapi.editor.ex.FoldingModelEx;
32 import com.intellij.openapi.editor.ex.util.EditorUtil;
33 import com.intellij.openapi.fileEditor.impl.text.CodeFoldingState;
34 import com.intellij.openapi.project.DumbAwareRunnable;
35 import com.intellij.openapi.project.DumbService;
36 import com.intellij.openapi.project.Project;
37 import com.intellij.openapi.startup.StartupManager;
38 import com.intellij.openapi.util.*;
39 import com.intellij.psi.PsiDocumentManager;
40 import com.intellij.psi.PsiElement;
41 import com.intellij.psi.PsiFile;
42 import com.intellij.ui.LightweightHint;
43 import com.intellij.util.containers.WeakList;
44 import org.jdom.Element;
45 import org.jetbrains.annotations.NotNull;
46 import org.jetbrains.annotations.Nullable;
47
48 import javax.swing.*;
49 import java.awt.*;
50 import java.awt.event.MouseEvent;
51 import java.util.List;
52
53 public class CodeFoldingManagerImpl extends CodeFoldingManager implements ProjectComponent {
54   private final Project myProject;
55
56   private final List<Document> myDocumentsWithFoldingInfo = new WeakList<Document>();
57
58   private final Key<DocumentFoldingInfo> myFoldingInfoInDocumentKey = Key.create("FOLDING_INFO_IN_DOCUMENT_KEY");
59   private static final Key<Boolean> FOLDING_STATE_INFO_IN_DOCUMENT_KEY = Key.create("FOLDING_STATE_IN_DOCUMENT");
60
61   CodeFoldingManagerImpl(Project project) {
62     myProject = project;
63     project.getMessageBus().connect().subscribe(DocumentBulkUpdateListener.TOPIC, new DocumentBulkUpdateListener.Adapter() {
64       @Override
65       public void updateStarted(@NotNull final Document doc) {
66         resetFoldingInfo(doc);
67       }
68     });
69   }
70
71   @Override
72   @NotNull
73   public String getComponentName() {
74     return "CodeFoldingManagerImpl";
75   }
76
77   @Override
78   public void initComponent() { }
79
80   @Override
81   public void disposeComponent() {
82     for (Document document : myDocumentsWithFoldingInfo) {
83       if (document != null) {
84         document.putUserData(myFoldingInfoInDocumentKey, null);
85       }
86     }
87   }
88
89   @Override
90   public void projectOpened() {
91     final EditorMouseMotionAdapter myMouseMotionListener = new EditorMouseMotionAdapter() {
92       LightweightHint myCurrentHint = null;
93       FoldRegion myCurrentFold = null;
94
95       @Override
96       public void mouseMoved(EditorMouseEvent e) {
97         if (myProject.isDisposed()) return;
98         HintManager hintManager = HintManager.getInstance();
99         if (hintManager != null && hintManager.hasShownHintsThatWillHideByOtherHint(false)) {
100           return;
101         }
102
103         if (e.getArea() != EditorMouseEventArea.FOLDING_OUTLINE_AREA) return;
104         LightweightHint hint = null;
105         try {
106           Editor editor = e.getEditor();
107           if (PsiDocumentManager.getInstance(myProject).isUncommited(editor.getDocument())) return;
108
109           MouseEvent mouseEvent = e.getMouseEvent();
110           FoldRegion fold = ((EditorEx)editor).getGutterComponentEx().findFoldingAnchorAt(mouseEvent.getX(), mouseEvent.getY());
111
112           if (fold == null || !fold.isValid()) return;
113           if (fold == myCurrentFold && myCurrentHint != null) {
114             hint = myCurrentHint;
115             return;
116           }
117
118           TextRange psiElementRange = EditorFoldingInfo.get(editor).getPsiElementRange(fold);
119           if (psiElementRange == null) return;
120
121           int textOffset = psiElementRange.getStartOffset();
122           // There is a possible case that target PSI element's offset is less than fold region offset (e.g. complete method is
123           // returned as PSI element for fold region that corresponds to java method code block). We don't want to show any hint
124           // if start of the current fold region is displayed.
125           Point foldStartXY = editor.visualPositionToXY(editor.offsetToVisualPosition(Math.max(textOffset, fold.getStartOffset())));
126           Rectangle visibleArea = editor.getScrollingModel().getVisibleArea();
127           if (visibleArea.y > foldStartXY.y) {
128             if (myCurrentHint != null) {
129               myCurrentHint.hide();
130               myCurrentHint = null;
131             }
132
133
134             // We want to show a hint with the top fold region content that is above the current viewport position.
135             // However, there is a possible case that complete region has a big height and only a little bottom part
136             // is shown at the moment. We can't just show hint with the whole top content because it would hide actual
137             // editor content, hence, we show max(2; available visual lines number) instead.
138             // P.S. '2' is used here in assumption that many java methods have javadocs which first line is just '/**'.
139             // So, it's not too useful to show only it even when available vertical space is not big enough.
140             int availableVisualLines = 2;
141             JComponent editorComponent = editor.getComponent();
142             Container editorComponentParent = editorComponent.getParent();
143             if (editorComponentParent != null) {
144               Container contentPane = editorComponent.getRootPane().getContentPane();
145               if (contentPane != null) {
146                 int y = SwingUtilities.convertPoint(editorComponentParent, editorComponent.getLocation(), contentPane).y;
147                 int visualLines = y / editor.getLineHeight();
148                 availableVisualLines = Math.max(availableVisualLines, visualLines);
149               }
150             }
151             int startVisualLine = editor.offsetToVisualPosition(textOffset).line;
152             int desiredEndVisualLine = Math.max(0, editor.xyToVisualPosition(new Point(0, visibleArea.y)).line - 1);
153             int endVisualLine = startVisualLine + availableVisualLines;
154             if (endVisualLine > desiredEndVisualLine) {
155               endVisualLine = desiredEndVisualLine;
156             }
157
158             // Show only the non-displayed top part of the target fold region
159             int endOffset = editor.logicalPositionToOffset(editor.visualToLogicalPosition(new VisualPosition(endVisualLine, 0)));
160             TextRange textRange = new UnfairTextRange(textOffset, endOffset);
161             hint = EditorFragmentComponent.showEditorFragmentHint(editor, textRange, true, true);
162             myCurrentFold = fold;
163             myCurrentHint = hint;
164           }
165         }
166         finally {
167           if (hint == null) {
168             if (myCurrentHint != null) {
169               myCurrentHint.hide();
170               myCurrentHint = null;
171             }
172             myCurrentFold = null;
173           }
174         }
175       }
176     };
177
178     StartupManager.getInstance(myProject).registerPostStartupActivity(new DumbAwareRunnable() {
179       @Override
180       public void run() {
181         EditorFactory.getInstance().getEventMulticaster().addEditorMouseMotionListener(myMouseMotionListener, myProject);
182       }
183     });
184   }
185
186   @Override
187   public void releaseFoldings(@NotNull Editor editor) {
188     ApplicationManagerEx.getApplicationEx().assertIsDispatchThread();
189     final Project project = editor.getProject();
190     if (project != null && (!project.equals(myProject) || !project.isOpen())) return;
191
192     Document document = editor.getDocument();
193     PsiFile file = PsiDocumentManager.getInstance(myProject).getPsiFile(document);
194     if (file == null || !file.getViewProvider().isPhysical() || !file.isValid()) return;
195     PsiDocumentManager.getInstance(myProject).commitDocument(document);
196
197     Editor[] otherEditors = EditorFactory.getInstance().getEditors(document, myProject);
198     if (otherEditors.length == 0 && !editor.isDisposed()) {
199       getDocumentFoldingInfo(document).loadFromEditor(editor);
200     }
201     EditorFoldingInfo.get(editor).dispose();
202   }
203
204   @Override
205   public void buildInitialFoldings(@NotNull final Editor editor) {
206     ApplicationManagerEx.getApplicationEx().assertIsDispatchThread();
207     final Project project = editor.getProject();
208     if (project == null || !project.equals(myProject)) return;
209
210     final Document document = editor.getDocument();
211     // Do not save/restore folding for code fragments
212     final PsiFile file = PsiDocumentManager.getInstance(myProject).getPsiFile(document);
213     if (file == null || !file.getViewProvider().isPhysical() && !ApplicationManager.getApplication().isUnitTestMode()) return;
214
215     final FoldingModelEx foldingModel = (FoldingModelEx)editor.getFoldingModel();
216     if (!foldingModel.isFoldingEnabled()) return;
217     if (project.isDisposed() || editor.isDisposed() || !file.isValid()) return;
218
219     if (EditorUtil.supportsDumbModeFolding(editor)) {
220       // Else: Postpone operation until first call of #updateFoldRegionsAsync with the [firstTime] param [true]
221       PsiDocumentManager.getInstance(myProject).commitDocument(document);
222       createInitFoldingAction(updateFoldRegions(editor, true, true), editor).run();
223     }
224   }
225
226   @NotNull
227   private Runnable createInitFoldingAction(@Nullable final Runnable updateFoldingRegionAction,
228                                            @NotNull final Editor editor) {
229     assert !DumbService.getInstance(myProject).isDumb() || EditorUtil.supportsDumbModeFolding(editor) : "Forbidden state for folding initialization";
230     return new Runnable() {
231       @Override
232       public void run() {
233         if (updateFoldingRegionAction != null) {
234           updateFoldingRegionAction.run();
235         }
236         if (myProject.isDisposed() || editor.isDisposed()) return;
237         // Restore folding state if need (it could be done concurrently)
238         if (!isFoldingsInitializedInEditor(editor)) {
239           editor.getFoldingModel().runBatchFoldingOperation(new Runnable() {
240             @Override
241             public void run() {
242               Document document = editor.getDocument();
243               DocumentFoldingInfo documentFoldingInfo = getDocumentFoldingInfo(document);
244
245               // Folding state could be changed in another editor
246               Editor[] editors = EditorFactory.getInstance().getEditors(document, editor.getProject());
247               for (Editor otherEditor : editors) {
248                 if (otherEditor == editor || !isFoldingsInitializedInEditor(otherEditor)) continue;
249                 // Any active editor overwrites folding from saved state (document info is empty for the case)
250                 documentFoldingInfo.loadFromEditor(otherEditor);
251                 break;
252               }
253               documentFoldingInfo.setToEditor(editor);
254
255               // Drop fording info for document. Next editor will load it from active editor.
256               // Then the last editor is closed, folding info saves into the document.
257               documentFoldingInfo.clear();
258               editor.getDocument().putUserData(FOLDING_STATE_INFO_IN_DOCUMENT_KEY, Boolean.TRUE);
259               editor.putUserData(FOLDING_STATE_INFO_IN_DOCUMENT_KEY, Boolean.TRUE);
260             }
261           });
262         }
263       }
264     };
265   }
266
267   @Override
268   public void projectClosed() {
269   }
270
271   @Override
272   @Nullable
273   public FoldRegion findFoldRegion(@NotNull Editor editor, int startOffset, int endOffset) {
274     return FoldingUtil.findFoldRegion(editor, startOffset, endOffset);
275   }
276
277   @Override
278   public FoldRegion[] getFoldRegionsAtOffset(@NotNull Editor editor, int offset) {
279     return FoldingUtil.getFoldRegionsAtOffset(editor, offset);
280   }
281
282   @Override
283   public void updateFoldRegions(@NotNull Editor editor) {
284     updateFoldRegions(editor, false);
285   }
286
287   public void updateFoldRegions(Editor editor, boolean quick) {
288     PsiDocumentManager.getInstance(myProject).commitDocument(editor.getDocument());
289     Runnable runnable = updateFoldRegions(editor, false, quick);
290     if (runnable != null) {
291       runnable.run();
292     }
293   }
294
295   @Override
296   public void forceDefaultState(@NotNull final Editor editor) {
297     PsiDocumentManager.getInstance(myProject).commitDocument(editor.getDocument());
298     Runnable runnable = updateFoldRegions(editor, true, false);
299     if (runnable != null) {
300       runnable.run();
301     }
302
303     final FoldRegion[] regions = editor.getFoldingModel().getAllFoldRegions();
304     editor.getFoldingModel().runBatchFoldingOperation(new Runnable() {
305       @Override
306       public void run() {
307         EditorFoldingInfo foldingInfo = EditorFoldingInfo.get(editor);
308         for (FoldRegion region : regions) {
309           PsiElement element = foldingInfo.getPsiElement(region);
310           if (element != null) {
311             region.setExpanded(!FoldingPolicy.isCollapseByDefault(element));
312           }
313         }
314       }
315     });
316   }
317
318   @Override
319   @Nullable
320   public Runnable updateFoldRegionsAsync(@NotNull Editor editor, boolean firstTime) {
321     if (firstTime) {
322       final Document document = editor.getDocument();
323       if (!isFoldingsInitializedInDocument(document)) {
324         // all editors need to be initialized
325         return new Runnable() {
326           @Override
327           public void run() {
328             final Editor[] editors = EditorFactory.getInstance().getEditors(document);
329             for(Editor anyEditor:editors) if (!isFoldingsInitializedInEditor(anyEditor)) {
330               createInitFoldingAction(updateFoldRegions(anyEditor, true, false), anyEditor).run();
331             }
332           }
333         };
334       }
335       if (!isFoldingsInitializedInEditor(editor)) {
336         // Restores folding state after regions initialization.
337         // That is called after the first folding pass
338         return createInitFoldingAction(updateFoldRegions(editor, true, false), editor);
339       }
340     }
341     return updateFoldRegions(editor, firstTime, false);
342   }
343
344   @Nullable
345   private Runnable updateFoldRegions(@NotNull Editor editor, boolean applyDefaultState, boolean quick) {
346     PsiFile file = PsiDocumentManager.getInstance(myProject).getPsiFile(editor.getDocument());
347     if (file != null) {
348       return FoldingUpdate.updateFoldRegions(editor, file, applyDefaultState, quick);
349     }
350     else {
351       return null;
352     }
353   }
354
355   @Override
356   public CodeFoldingState saveFoldingState(@NotNull Editor editor) {
357     ApplicationManager.getApplication().assertIsDispatchThread();
358     DocumentFoldingInfo info = getDocumentFoldingInfo(editor.getDocument());
359     if (isFoldingsInitializedInEditor(editor)) {
360       info.loadFromEditor(editor);
361     }
362     return info;
363   }
364
365   @Override
366   public void restoreFoldingState(@NotNull Editor editor, @NotNull CodeFoldingState state) {
367     ApplicationManager.getApplication().assertIsDispatchThread();
368     if (isFoldingsInitializedInEditor(editor)) {
369       ((DocumentFoldingInfo)state).setToEditor(editor);
370     }
371   }
372
373   @Override
374   public void writeFoldingState(@NotNull CodeFoldingState state, @NotNull Element element) throws WriteExternalException {
375     ((DocumentFoldingInfo)state).writeExternal(element);
376   }
377
378   @Override
379   public CodeFoldingState readFoldingState(@NotNull Element element, @NotNull Document document) {
380     DocumentFoldingInfo info = getDocumentFoldingInfo(document);
381     info.readExternal(element);
382     return info;
383   }
384
385   @NotNull
386   private DocumentFoldingInfo getDocumentFoldingInfo(@NotNull Document document) {
387     DocumentFoldingInfo info = document.getUserData(myFoldingInfoInDocumentKey);
388     if (info == null) {
389       info = new DocumentFoldingInfo(myProject, document);
390       DocumentFoldingInfo written = ((UserDataHolderEx)document).putUserDataIfAbsent(myFoldingInfoInDocumentKey, info);
391       if (written == info) {
392         myDocumentsWithFoldingInfo.add(document);
393       }
394       else {
395         info = written;
396       }
397     }
398     return info;
399   }
400
401   private static void resetFoldingInfo(@NotNull final Document document) {
402     if (isFoldingsInitializedInDocument(document)) {
403       final Editor[] editors = EditorFactory.getInstance().getEditors(document);
404       for(Editor editor:editors) {
405         EditorFoldingInfo.resetInfo(editor);
406       }
407       document.putUserData(FOLDING_STATE_INFO_IN_DOCUMENT_KEY, null);
408     }
409   }
410
411   static boolean isFoldingsInitializedInDocument(@NotNull Document document) {
412     return Boolean.TRUE.equals(document.getUserData(FOLDING_STATE_INFO_IN_DOCUMENT_KEY));
413   }
414
415   static boolean isFoldingsInitializedInEditor(@NotNull Editor editor) {
416     return Boolean.TRUE.equals(editor.getUserData(FOLDING_STATE_INFO_IN_DOCUMENT_KEY));
417   }
418 }