blob: 9a5bd2cbbe6b3bc69745154db53e4e52130277ea [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.codeInsight.folding.impl;
import com.intellij.codeInsight.folding.CodeFoldingManager;
import com.intellij.codeInsight.hint.EditorFragmentComponent;
import com.intellij.codeInsight.hint.HintManager;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ex.ApplicationManagerEx;
import com.intellij.openapi.components.ProjectComponent;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.event.EditorMouseEvent;
import com.intellij.openapi.editor.event.EditorMouseEventArea;
import com.intellij.openapi.editor.event.EditorMouseMotionAdapter;
import com.intellij.openapi.editor.ex.DocumentBulkUpdateListener;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.ex.FoldingModelEx;
import com.intellij.openapi.fileEditor.impl.text.CodeFoldingState;
import com.intellij.openapi.project.DumbAwareRunnable;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.startup.StartupManager;
import com.intellij.openapi.util.*;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.ui.LightweightHint;
import com.intellij.util.containers.WeakList;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.util.List;
public class CodeFoldingManagerImpl extends CodeFoldingManager implements ProjectComponent {
private final Project myProject;
private final List<Document> myDocumentsWithFoldingInfo = new WeakList<Document>();
private final Key<DocumentFoldingInfo> myFoldingInfoInDocumentKey = Key.create("FOLDING_INFO_IN_DOCUMENT_KEY");
private static final Key<Boolean> FOLDING_STATE_KEY = Key.create("FOLDING_STATE_KEY");
CodeFoldingManagerImpl(Project project) {
myProject = project;
project.getMessageBus().connect().subscribe(DocumentBulkUpdateListener.TOPIC, new DocumentBulkUpdateListener.Adapter() {
@Override
public void updateStarted(@NotNull final Document doc) {
resetFoldingInfo(doc);
}
});
}
@Override
@NotNull
public String getComponentName() {
return "CodeFoldingManagerImpl";
}
@Override
public void initComponent() { }
@Override
public void disposeComponent() {
for (Document document : myDocumentsWithFoldingInfo) {
if (document != null) {
document.putUserData(myFoldingInfoInDocumentKey, null);
}
}
}
@Override
public void projectOpened() {
final EditorMouseMotionAdapter myMouseMotionListener = new EditorMouseMotionAdapter() {
LightweightHint myCurrentHint = null;
FoldRegion myCurrentFold = null;
@Override
public void mouseMoved(EditorMouseEvent e) {
if (myProject.isDisposed()) return;
HintManager hintManager = HintManager.getInstance();
if (hintManager != null && hintManager.hasShownHintsThatWillHideByOtherHint(false)) {
return;
}
if (e.getArea() != EditorMouseEventArea.FOLDING_OUTLINE_AREA) return;
LightweightHint hint = null;
try {
Editor editor = e.getEditor();
if (PsiDocumentManager.getInstance(myProject).isUncommited(editor.getDocument())) return;
MouseEvent mouseEvent = e.getMouseEvent();
FoldRegion fold = ((EditorEx)editor).getGutterComponentEx().findFoldingAnchorAt(mouseEvent.getX(), mouseEvent.getY());
if (fold == null || !fold.isValid()) return;
if (fold == myCurrentFold && myCurrentHint != null) {
hint = myCurrentHint;
return;
}
TextRange psiElementRange = EditorFoldingInfo.get(editor).getPsiElementRange(fold);
if (psiElementRange == null) return;
int textOffset = psiElementRange.getStartOffset();
// There is a possible case that target PSI element's offset is less than fold region offset (e.g. complete method is
// returned as PSI element for fold region that corresponds to java method code block). We don't want to show any hint
// if start of the current fold region is displayed.
Point foldStartXY = editor.visualPositionToXY(editor.offsetToVisualPosition(Math.max(textOffset, fold.getStartOffset())));
Rectangle visibleArea = editor.getScrollingModel().getVisibleArea();
if (visibleArea.y > foldStartXY.y) {
if (myCurrentHint != null) {
myCurrentHint.hide();
myCurrentHint = null;
}
// We want to show a hint with the top fold region content that is above the current viewport position.
// However, there is a possible case that complete region has a big height and only a little bottom part
// is shown at the moment. We can't just show hint with the whole top content because it would hide actual
// editor content, hence, we show max(2; available visual lines number) instead.
// P.S. '2' is used here in assumption that many java methods have javadocs which first line is just '/**'.
// So, it's not too useful to show only it even when available vertical space is not big enough.
int availableVisualLines = 2;
JComponent editorComponent = editor.getComponent();
Container editorComponentParent = editorComponent.getParent();
if (editorComponentParent != null) {
Container contentPane = editorComponent.getRootPane().getContentPane();
if (contentPane != null) {
int y = SwingUtilities.convertPoint(editorComponentParent, editorComponent.getLocation(), contentPane).y;
int visualLines = y / editor.getLineHeight();
availableVisualLines = Math.max(availableVisualLines, visualLines);
}
}
int startVisualLine = editor.offsetToVisualPosition(textOffset).line;
int desiredEndVisualLine = Math.max(0, editor.xyToVisualPosition(new Point(0, visibleArea.y)).line - 1);
int endVisualLine = startVisualLine + availableVisualLines;
if (endVisualLine > desiredEndVisualLine) {
endVisualLine = desiredEndVisualLine;
}
// Show only the non-displayed top part of the target fold region
int endOffset = editor.logicalPositionToOffset(editor.visualToLogicalPosition(new VisualPosition(endVisualLine, 0)));
TextRange textRange = new UnfairTextRange(textOffset, endOffset);
hint = EditorFragmentComponent.showEditorFragmentHint(editor, textRange, true, true);
myCurrentFold = fold;
myCurrentHint = hint;
}
}
finally {
if (hint == null) {
if (myCurrentHint != null) {
myCurrentHint.hide();
myCurrentHint = null;
}
myCurrentFold = null;
}
}
}
};
StartupManager.getInstance(myProject).registerPostStartupActivity(new DumbAwareRunnable() {
@Override
public void run() {
EditorFactory.getInstance().getEventMulticaster().addEditorMouseMotionListener(myMouseMotionListener, myProject);
}
});
}
@Override
public void releaseFoldings(@NotNull Editor editor) {
ApplicationManagerEx.getApplicationEx().assertIsDispatchThread();
final Project project = editor.getProject();
if (project != null && (!project.equals(myProject) || !project.isOpen())) return;
Document document = editor.getDocument();
PsiFile file = PsiDocumentManager.getInstance(myProject).getPsiFile(document);
if (file == null || !file.getViewProvider().isPhysical() || !file.isValid()) return;
PsiDocumentManager.getInstance(myProject).commitDocument(document);
Editor[] otherEditors = EditorFactory.getInstance().getEditors(document, myProject);
if (otherEditors.length == 0 && !editor.isDisposed()) {
getDocumentFoldingInfo(document).loadFromEditor(editor);
}
EditorFoldingInfo.get(editor).dispose();
}
@Override
public void buildInitialFoldings(@NotNull final Editor editor) {
final Project project = editor.getProject();
if (project == null || !project.equals(myProject) || editor.isDisposed()) return;
if (!((FoldingModelEx)editor.getFoldingModel()).isFoldingEnabled()) return;
if (!FoldingUpdate.supportsDumbModeFolding(editor)) return;
Document document = editor.getDocument();
PsiDocumentManager.getInstance(myProject).commitDocument(document);
CodeFoldingState foldingState = buildInitialFoldings(document);
if (foldingState != null) {
foldingState.setToEditor(editor);
}
}
@Nullable
@Override
public CodeFoldingState buildInitialFoldings(@NotNull final Document document) {
if (myProject.isDisposed()) {
return null;
}
ApplicationManager.getApplication().assertReadAccessAllowed();
PsiDocumentManager psiDocumentManager = PsiDocumentManager.getInstance(myProject);
if (psiDocumentManager.isUncommited(document)) {
// skip building foldings for uncommitted document, CodeFoldingPass invoked by daemon will do it later
return null;
}
//Do not save/restore folding for code fragments
final PsiFile file = psiDocumentManager.getPsiFile(document);
if (file == null || !file.isValid() || !file.getViewProvider().isPhysical() && !ApplicationManager.getApplication().isUnitTestMode()) {
return null;
}
final FoldingUpdate.FoldingMap foldingMap = FoldingUpdate.getFoldingsFor(myProject, file, document, true);
return new CodeFoldingState() {
@Override
public void setToEditor(@NotNull final Editor editor) {
ApplicationManagerEx.getApplicationEx().assertIsDispatchThread();
if (myProject.isDisposed() || editor.isDisposed()) return;
final FoldingModelEx foldingModel = (FoldingModelEx)editor.getFoldingModel();
if (!foldingModel.isFoldingEnabled()) return;
if (isFoldingsInitializedInEditor(editor)) return;
if (DumbService.isDumb(myProject) && !FoldingUpdate.supportsDumbModeFolding(editor)) return;
foldingModel.runBatchFoldingOperationDoNotCollapseCaret(new UpdateFoldRegionsOperation(myProject, editor, file, foldingMap, true, false));
initFolding(editor);
}
};
}
private void initFolding(@NotNull final Editor editor) {
final Document document = editor.getDocument();
editor.getFoldingModel().runBatchFoldingOperation(new Runnable() {
@Override
public void run() {
DocumentFoldingInfo documentFoldingInfo = getDocumentFoldingInfo(document);
Editor[] editors = EditorFactory.getInstance().getEditors(document, myProject);
for (Editor otherEditor : editors) {
if (otherEditor == editor || !isFoldingsInitializedInEditor(otherEditor)) continue;
documentFoldingInfo.loadFromEditor(otherEditor);
break;
}
documentFoldingInfo.setToEditor(editor);
documentFoldingInfo.clear();
document.putUserData(FOLDING_STATE_KEY, Boolean.TRUE);
editor.putUserData(FOLDING_STATE_KEY, Boolean.TRUE);
}
});
}
@Override
public void projectClosed() {
}
@Override
@Nullable
public FoldRegion findFoldRegion(@NotNull Editor editor, int startOffset, int endOffset) {
return FoldingUtil.findFoldRegion(editor, startOffset, endOffset);
}
@Override
public FoldRegion[] getFoldRegionsAtOffset(@NotNull Editor editor, int offset) {
return FoldingUtil.getFoldRegionsAtOffset(editor, offset);
}
@Override
public void updateFoldRegions(@NotNull Editor editor) {
updateFoldRegions(editor, false);
}
public void updateFoldRegions(Editor editor, boolean quick) {
PsiDocumentManager.getInstance(myProject).commitDocument(editor.getDocument());
Runnable runnable = updateFoldRegions(editor, false, quick);
if (runnable != null) {
runnable.run();
}
}
@Override
public void forceDefaultState(@NotNull final Editor editor) {
PsiDocumentManager.getInstance(myProject).commitDocument(editor.getDocument());
Runnable runnable = updateFoldRegions(editor, true, false);
if (runnable != null) {
runnable.run();
}
final FoldRegion[] regions = editor.getFoldingModel().getAllFoldRegions();
editor.getFoldingModel().runBatchFoldingOperation(new Runnable() {
@Override
public void run() {
EditorFoldingInfo foldingInfo = EditorFoldingInfo.get(editor);
for (FoldRegion region : regions) {
PsiElement element = foldingInfo.getPsiElement(region);
if (element != null) {
region.setExpanded(!FoldingPolicy.isCollapseByDefault(element));
}
}
}
});
}
@Override
@Nullable
public Runnable updateFoldRegionsAsync(@NotNull final Editor editor, final boolean firstTime) {
final Runnable runnable = updateFoldRegions(editor, firstTime, false);
return new Runnable() {
@Override
public void run() {
if (runnable != null) {
runnable.run();
}
if (firstTime && !isFoldingsInitializedInEditor(editor)) {
initFolding(editor);
}
}
};
}
@Nullable
private Runnable updateFoldRegions(@NotNull Editor editor, boolean applyDefaultState, boolean quick) {
PsiFile file = PsiDocumentManager.getInstance(myProject).getPsiFile(editor.getDocument());
if (file != null) {
return FoldingUpdate.updateFoldRegions(editor, file, applyDefaultState, quick);
}
else {
return null;
}
}
@Override
public CodeFoldingState saveFoldingState(@NotNull Editor editor) {
ApplicationManager.getApplication().assertIsDispatchThread();
DocumentFoldingInfo info = getDocumentFoldingInfo(editor.getDocument());
if (isFoldingsInitializedInEditor(editor)) {
info.loadFromEditor(editor);
}
return info;
}
@Override
public void restoreFoldingState(@NotNull Editor editor, @NotNull CodeFoldingState state) {
ApplicationManager.getApplication().assertIsDispatchThread();
if (isFoldingsInitializedInEditor(editor)) {
state.setToEditor(editor);
}
}
@Override
public void writeFoldingState(@NotNull CodeFoldingState state, @NotNull Element element) throws WriteExternalException {
if (state instanceof DocumentFoldingInfo) {
((DocumentFoldingInfo)state).writeExternal(element);
}
else {
throw new WriteExternalException();
}
}
@Override
public CodeFoldingState readFoldingState(@NotNull Element element, @NotNull Document document) {
DocumentFoldingInfo info = getDocumentFoldingInfo(document);
info.readExternal(element);
return info;
}
@NotNull
private DocumentFoldingInfo getDocumentFoldingInfo(@NotNull Document document) {
DocumentFoldingInfo info = document.getUserData(myFoldingInfoInDocumentKey);
if (info == null) {
info = new DocumentFoldingInfo(myProject, document);
DocumentFoldingInfo written = ((UserDataHolderEx)document).putUserDataIfAbsent(myFoldingInfoInDocumentKey, info);
if (written == info) {
myDocumentsWithFoldingInfo.add(document);
}
else {
info = written;
}
}
return info;
}
private static void resetFoldingInfo(@NotNull final Document document) {
if (isFoldingsInitializedInDocument(document)) {
final Editor[] editors = EditorFactory.getInstance().getEditors(document);
for(Editor editor:editors) {
EditorFoldingInfo.resetInfo(editor);
}
document.putUserData(FOLDING_STATE_KEY, null);
}
}
static boolean isFoldingsInitializedInDocument(@NotNull Document document) {
return Boolean.TRUE.equals(document.getUserData(FOLDING_STATE_KEY));
}
static boolean isFoldingsInitializedInEditor(@NotNull Editor editor) {
return Boolean.TRUE.equals(editor.getUserData(FOLDING_STATE_KEY));
}
}