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