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.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;
50 import java.awt.event.MouseEvent;
51 import java.util.List;
53 public class CodeFoldingManagerImpl extends CodeFoldingManager implements ProjectComponent {
54 private final Project myProject;
56 private final List<Document> myDocumentsWithFoldingInfo = new WeakList<Document>();
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");
61 CodeFoldingManagerImpl(Project project) {
63 project.getMessageBus().connect().subscribe(DocumentBulkUpdateListener.TOPIC, new DocumentBulkUpdateListener.Adapter() {
65 public void updateStarted(@NotNull final Document doc) {
66 resetFoldingInfo(doc);
73 public String getComponentName() {
74 return "CodeFoldingManagerImpl";
78 public void initComponent() { }
81 public void disposeComponent() {
82 for (Document document : myDocumentsWithFoldingInfo) {
83 if (document != null) {
84 document.putUserData(myFoldingInfoInDocumentKey, null);
90 public void projectOpened() {
91 final EditorMouseMotionAdapter myMouseMotionListener = new EditorMouseMotionAdapter() {
92 LightweightHint myCurrentHint = null;
93 FoldRegion myCurrentFold = null;
96 public void mouseMoved(EditorMouseEvent e) {
97 if (myProject.isDisposed()) return;
98 HintManager hintManager = HintManager.getInstance();
99 if (hintManager != null && hintManager.hasShownHintsThatWillHideByOtherHint(false)) {
103 if (e.getArea() != EditorMouseEventArea.FOLDING_OUTLINE_AREA) return;
104 LightweightHint hint = null;
106 Editor editor = e.getEditor();
107 if (PsiDocumentManager.getInstance(myProject).isUncommited(editor.getDocument())) return;
109 MouseEvent mouseEvent = e.getMouseEvent();
110 FoldRegion fold = ((EditorEx)editor).getGutterComponentEx().findFoldingAnchorAt(mouseEvent.getX(), mouseEvent.getY());
112 if (fold == null || !fold.isValid()) return;
113 if (fold == myCurrentFold && myCurrentHint != null) {
114 hint = myCurrentHint;
118 TextRange psiElementRange = EditorFoldingInfo.get(editor).getPsiElementRange(fold);
119 if (psiElementRange == null) return;
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;
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);
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;
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;
168 if (myCurrentHint != null) {
169 myCurrentHint.hide();
170 myCurrentHint = null;
172 myCurrentFold = null;
178 StartupManager.getInstance(myProject).registerPostStartupActivity(new DumbAwareRunnable() {
181 EditorFactory.getInstance().getEventMulticaster().addEditorMouseMotionListener(myMouseMotionListener, myProject);
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;
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);
197 Editor[] otherEditors = EditorFactory.getInstance().getEditors(document, myProject);
198 if (otherEditors.length == 0 && !editor.isDisposed()) {
199 getDocumentFoldingInfo(document).loadFromEditor(editor);
201 EditorFoldingInfo.get(editor).dispose();
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;
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;
215 final FoldingModelEx foldingModel = (FoldingModelEx)editor.getFoldingModel();
216 if (!foldingModel.isFoldingEnabled()) return;
217 if (project.isDisposed() || editor.isDisposed() || !file.isValid()) return;
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();
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() {
233 if (updateFoldingRegionAction != null) {
234 updateFoldingRegionAction.run();
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() {
242 Document document = editor.getDocument();
243 DocumentFoldingInfo documentFoldingInfo = getDocumentFoldingInfo(document);
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);
253 documentFoldingInfo.setToEditor(editor);
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);
268 public void projectClosed() {
273 public FoldRegion findFoldRegion(@NotNull Editor editor, int startOffset, int endOffset) {
274 return FoldingUtil.findFoldRegion(editor, startOffset, endOffset);
278 public FoldRegion[] getFoldRegionsAtOffset(@NotNull Editor editor, int offset) {
279 return FoldingUtil.getFoldRegionsAtOffset(editor, offset);
283 public void updateFoldRegions(@NotNull Editor editor) {
284 updateFoldRegions(editor, false);
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) {
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) {
303 final FoldRegion[] regions = editor.getFoldingModel().getAllFoldRegions();
304 editor.getFoldingModel().runBatchFoldingOperation(new Runnable() {
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));
320 public Runnable updateFoldRegionsAsync(@NotNull Editor editor, boolean firstTime) {
322 final Document document = editor.getDocument();
323 if (!isFoldingsInitializedInDocument(document)) {
324 // all editors need to be initialized
325 return new Runnable() {
328 final Editor[] editors = EditorFactory.getInstance().getEditors(document);
329 for(Editor anyEditor:editors) if (!isFoldingsInitializedInEditor(anyEditor)) {
330 createInitFoldingAction(updateFoldRegions(anyEditor, true, false), anyEditor).run();
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);
341 return updateFoldRegions(editor, firstTime, false);
345 private Runnable updateFoldRegions(@NotNull Editor editor, boolean applyDefaultState, boolean quick) {
346 PsiFile file = PsiDocumentManager.getInstance(myProject).getPsiFile(editor.getDocument());
348 return FoldingUpdate.updateFoldRegions(editor, file, applyDefaultState, quick);
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);
366 public void restoreFoldingState(@NotNull Editor editor, @NotNull CodeFoldingState state) {
367 ApplicationManager.getApplication().assertIsDispatchThread();
368 if (isFoldingsInitializedInEditor(editor)) {
369 ((DocumentFoldingInfo)state).setToEditor(editor);
374 public void writeFoldingState(@NotNull CodeFoldingState state, @NotNull Element element) throws WriteExternalException {
375 ((DocumentFoldingInfo)state).writeExternal(element);
379 public CodeFoldingState readFoldingState(@NotNull Element element, @NotNull Document document) {
380 DocumentFoldingInfo info = getDocumentFoldingInfo(document);
381 info.readExternal(element);
386 private DocumentFoldingInfo getDocumentFoldingInfo(@NotNull Document document) {
387 DocumentFoldingInfo info = document.getUserData(myFoldingInfoInDocumentKey);
389 info = new DocumentFoldingInfo(myProject, document);
390 DocumentFoldingInfo written = ((UserDataHolderEx)document).putUserDataIfAbsent(myFoldingInfoInDocumentKey, info);
391 if (written == info) {
392 myDocumentsWithFoldingInfo.add(document);
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);
407 document.putUserData(FOLDING_STATE_INFO_IN_DOCUMENT_KEY, null);
411 static boolean isFoldingsInitializedInDocument(@NotNull Document document) {
412 return Boolean.TRUE.equals(document.getUserData(FOLDING_STATE_INFO_IN_DOCUMENT_KEY));
415 static boolean isFoldingsInitializedInEditor(@NotNull Editor editor) {
416 return Boolean.TRUE.equals(editor.getUserData(FOLDING_STATE_INFO_IN_DOCUMENT_KEY));