blob: bbee880bfde10eab4e88703af34c1b5f1ac20b70 [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.xml.breadcrumbs;
import com.intellij.application.options.editor.WebEditorOptions;
import com.intellij.ide.ui.UISettings;
import com.intellij.ide.ui.UISettingsListener;
import com.intellij.lang.Language;
import com.intellij.lang.xml.XMLLanguage;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.LogicalPosition;
import com.intellij.openapi.editor.ScrollType;
import com.intellij.openapi.editor.colors.EditorFontType;
import com.intellij.openapi.editor.event.CaretAdapter;
import com.intellij.openapi.editor.event.CaretEvent;
import com.intellij.openapi.editor.event.CaretListener;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vcs.FileStatusListener;
import com.intellij.openapi.vcs.FileStatusManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.update.MergingUpdateQueue;
import com.intellij.util.ui.update.UiNotifyConnector;
import com.intellij.util.ui.update.Update;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.PriorityQueue;
/**
* @author spleaner
*/
public class BreadcrumbsXmlWrapper implements BreadcrumbsItemListener<BreadcrumbsPsiItem>, Disposable {
private final BreadcrumbsComponent<BreadcrumbsPsiItem> myComponent;
private final Project myProject;
private Editor myEditor;
private final VirtualFile myFile;
private boolean myUserCaretChange;
private final MergingUpdateQueue myQueue;
private final BreadcrumbsInfoProvider myInfoProvider;
private final JPanel myWrapperPanel;
public static final Key<BreadcrumbsXmlWrapper> BREADCRUMBS_COMPONENT_KEY = new Key<BreadcrumbsXmlWrapper>("BREADCRUMBS_KEY");
public BreadcrumbsXmlWrapper(@NotNull final Editor editor) {
myEditor = editor;
myEditor.putUserData(BREADCRUMBS_COMPONENT_KEY, this);
final Project project = editor.getProject();
assert project != null;
myProject = project;
Document document = myEditor.getDocument();
myFile = FileDocumentManager.getInstance().getFile(document);
final FileStatusManager manager = FileStatusManager.getInstance(project);
manager.addFileStatusListener(new FileStatusListener() {
@Override
public void fileStatusesChanged() {
updateCrumbs();
}
@Override
public void fileStatusChanged(@NotNull final VirtualFile virtualFile) {
}
}, this);
UISettings.getInstance().addUISettingsListener(new UISettingsListener() {
@Override
public void uiSettingsChanged(UISettings source) {
updateCrumbs();
}
}, this);
myInfoProvider = findInfoProvider(findViewProvider(myFile, myProject));
final CaretListener caretListener = new CaretAdapter() {
@Override
public void caretPositionChanged(final CaretEvent e) {
if (myUserCaretChange) {
queueUpdate(editor);
}
myUserCaretChange = true;
}
};
editor.getCaretModel().addCaretListener(caretListener);
Disposer.register(this, new Disposable() {
@Override
public void dispose() {
editor.getCaretModel().removeCaretListener(caretListener);
}
});
PsiManager.getInstance(project).addPsiTreeChangeListener(new PsiTreeChangeAdapter() {
@Override
public void propertyChanged(@NotNull PsiTreeChangeEvent event) {
PsiFile psiFile = event.getFile();
VirtualFile file = psiFile == null ? null : psiFile.getVirtualFile();
if (!Comparing.equal(file, myFile)) return;
queueUpdate(editor);
}
@Override
public void childrenChanged(@NotNull PsiTreeChangeEvent event) {
propertyChanged(event);
}
@Override
public void childMoved(@NotNull PsiTreeChangeEvent event) {
propertyChanged(event);
}
@Override
public void childReplaced(@NotNull PsiTreeChangeEvent event) {
propertyChanged(event);
}
@Override
public void childRemoved(@NotNull PsiTreeChangeEvent event) {
propertyChanged(event);
}
@Override
public void childAdded(@NotNull PsiTreeChangeEvent event) {
propertyChanged(event);
}
},this);
myComponent = new BreadcrumbsComponent<BreadcrumbsPsiItem>();
myComponent.addBreadcrumbsItemListener(this);
final Font editorFont = editor.getColorsScheme().getFont(EditorFontType.PLAIN);
myComponent.setFont(editorFont.deriveFont(Font.PLAIN, editorFont.getSize2D()));
final ComponentAdapter resizeListener = new ComponentAdapter() {
@Override
public void componentResized(final ComponentEvent e) {
queueUpdate(editor);
}
};
myComponent.addComponentListener(resizeListener);
Disposer.register(this, new Disposable() {
@Override
public void dispose() {
myComponent.removeComponentListener(resizeListener);
}
});
myQueue = new MergingUpdateQueue("Breadcrumbs.Queue", 200, true, myComponent);
myQueue.queue(new MyUpdate(this, editor));
Disposer.register(this, new UiNotifyConnector(myComponent, myQueue));
Disposer.register(this, myQueue);
myWrapperPanel = new JPanel();
myWrapperPanel.setLayout(new BorderLayout());
myWrapperPanel.setBorder(BorderFactory.createEmptyBorder(2, 2, 1, 2));
myWrapperPanel.setOpaque(false);
myWrapperPanel.add(myComponent, BorderLayout.CENTER);
}
private void updateCrumbs() {
if (myComponent != null && myEditor != null) {
final Font editorFont = myEditor.getColorsScheme().getFont(EditorFontType.PLAIN);
myComponent.setFont(editorFont.deriveFont(Font.PLAIN, editorFont.getSize2D()));
updateCrumbs(myEditor.getCaretModel().getLogicalPosition());
}
}
public void queueUpdate(Editor editor) {
myQueue.cancelAllUpdates();
myQueue.queue(new MyUpdate(this, editor));
}
private void moveEditorCaretTo(@NotNull final PsiElement element) {
if (element.isValid()) {
setUserCaretChange(false);
myEditor.getCaretModel().moveToOffset(element.getTextOffset());
myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
}
}
@Nullable
private static BreadcrumbsInfoProvider findProviderForElement(@NotNull final PsiElement element,
final BreadcrumbsInfoProvider defaultProvider) {
final BreadcrumbsInfoProvider provider = getInfoProvider(element.getLanguage());
return provider == null ? defaultProvider : provider;
}
private static PsiElement[] toPsiElementArray(Collection<BreadcrumbsPsiItem> items) {
final PsiElement[] elements = new PsiElement[items.size()];
int index = 0;
for (BreadcrumbsPsiItem item : items) {
elements[index++] = item.getPsiElement();
}
return elements;
}
@Nullable
private static CrumbPresentation[] getCrumbPresentations(final PsiElement[] elements) {
for (BreadcrumbsPresentationProvider provider : BreadcrumbsPresentationProvider.EP_NAME.getExtensions()) {
final CrumbPresentation[] presentations = provider.getCrumbPresentations(elements);
if (presentations != null) {
return presentations;
}
}
return null;
}
private void setUserCaretChange(final boolean userCaretChange) {
myUserCaretChange = userCaretChange;
}
@Nullable
private static LinkedList<BreadcrumbsPsiItem> getPresentableLineElements(@NotNull final LogicalPosition position,
final VirtualFile file,
final Editor editor,
final Project project,
final BreadcrumbsInfoProvider defaultInfoProvider) {
final LinkedList<BreadcrumbsPsiItem> result =
getLineElements(editor.logicalPositionToOffset(position), file, project, defaultInfoProvider);
if (result == null) return null;
final PsiElement[] elements = toPsiElementArray(result);
final CrumbPresentation[] presentations = getCrumbPresentations(elements);
if (presentations != null) {
int i = 0;
for (BreadcrumbsPsiItem item : result) {
item.setPresentation(presentations[i++]);
}
}
return result;
}
@Nullable
public static PsiElement[] getLinePsiElements(int offset, VirtualFile file, Project project, BreadcrumbsInfoProvider infoProvider) {
final LinkedList<BreadcrumbsPsiItem> lineElements = getLineElements(offset, file, project, infoProvider);
return lineElements != null ? toPsiElementArray(lineElements) : null;
}
@Nullable
private static LinkedList<BreadcrumbsPsiItem> getLineElements(final int offset,
VirtualFile file,
Project project,
BreadcrumbsInfoProvider defaultInfoProvider) {
PsiElement element = findFirstBreadcrumbedElement(offset, file, project, defaultInfoProvider);
if (element == null) return null;
final LinkedList<BreadcrumbsPsiItem> result = new LinkedList<BreadcrumbsPsiItem>();
while (element != null) {
BreadcrumbsInfoProvider provider = findProviderForElement(element, defaultInfoProvider);
if (provider != null && provider.acceptElement(element)) {
result.addFirst(new BreadcrumbsPsiItem(element, provider));
}
element = (provider != null) ? provider.getParent(element) : element.getParent();
}
return result;
}
@Nullable
private static PsiElement findFirstBreadcrumbedElement(final int offset,
final VirtualFile file,
final Project project,
final BreadcrumbsInfoProvider defaultInfoProvider) {
if (file == null || !file.isValid()) return null;
PriorityQueue<PsiElement> leafs = new PriorityQueue<PsiElement>(3, new Comparator<PsiElement>() {
@Override
public int compare(final PsiElement o1, final PsiElement o2) {
return o2.getTextRange().getStartOffset() - o1.getTextRange().getStartOffset();
}
});
FileViewProvider viewProvider = findViewProvider(file, project);
if (viewProvider == null) return null;
for (final Language language : viewProvider.getLanguages()) {
ContainerUtil.addIfNotNull(viewProvider.findElementAt(offset, language), leafs);
}
while (!leafs.isEmpty()) {
final PsiElement element = leafs.remove();
if (!element.isValid()) continue;
BreadcrumbsInfoProvider provider = findProviderForElement(element, defaultInfoProvider);
if (provider != null && provider.acceptElement(element)) {
return element;
}
if (!(element instanceof PsiFile)) {
ContainerUtil.addIfNotNull(element.getParent(), leafs);
}
}
return null;
}
@Nullable
private static FileViewProvider findViewProvider(final VirtualFile file, final Project project) {
if (file == null) return null;
return PsiManager.getInstance(project).findViewProvider(file);
}
private void updateCrumbs(final LogicalPosition position) {
if (myFile != null && myEditor != null) {
if (PsiDocumentManager.getInstance(myProject).isUncommited(myEditor.getDocument())) {
return;
}
myComponent.setItems(getPresentableLineElements(position, myFile, myEditor, myProject, myInfoProvider));
}
}
@Nullable
public static BreadcrumbsInfoProvider findInfoProvider(@Nullable FileViewProvider viewProvider) {
BreadcrumbsInfoProvider provider = null;
if (viewProvider != null) {
final WebEditorOptions webEditorOptions = WebEditorOptions.getInstance();
final Language baseLang = viewProvider.getBaseLanguage();
provider = getInfoProvider(baseLang);
if (!webEditorOptions.isBreadcrumbsEnabledInXml() && baseLang == XMLLanguage.INSTANCE) return null;
if (!webEditorOptions.isBreadcrumbsEnabled() && baseLang != XMLLanguage.INSTANCE) return null;
if (provider == null) {
for (final Language language : viewProvider.getLanguages()) {
provider = getInfoProvider(language);
if (provider != null) {
break;
}
}
}
}
return provider;
}
public JComponent getComponent() {
return myWrapperPanel;
}
@Override
public void itemSelected(@NotNull final BreadcrumbsPsiItem item, final int modifiers) {
final PsiElement psiElement = item.getPsiElement();
moveEditorCaretTo(psiElement);
if ((modifiers & Event.SHIFT_MASK) == Event.SHIFT_MASK || (modifiers & Event.META_MASK) == Event.META_MASK) {
final TextRange range = psiElement.getTextRange();
myEditor.getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset());
}
}
@Nullable
public static BreadcrumbsXmlWrapper getBreadcrumbsComponent(@NotNull Editor editor) {
return editor.getUserData(BREADCRUMBS_COMPONENT_KEY);
}
@Override
public void dispose() {
myEditor.putUserData(BREADCRUMBS_COMPONENT_KEY, null);
myEditor = null;
}
@Nullable
private static BreadcrumbsInfoProvider getInfoProvider(@NotNull final Language language) {
for (final BreadcrumbsInfoProvider provider : Extensions.getExtensions(BreadcrumbsInfoProvider.EP_NAME)) {
for (final Language language1 : provider.getLanguages()) {
if (language.isKindOf(language1)) {
return provider;
}
}
}
return null;
}
private class MyUpdate extends Update {
private final BreadcrumbsXmlWrapper myBreadcrumbsComponent;
private final Editor myEditor;
public MyUpdate(@NonNls final BreadcrumbsXmlWrapper c, @NotNull final Editor editor) {
super(c);
myBreadcrumbsComponent = c;
myEditor = editor;
}
@Override
public void run() {
myBreadcrumbsComponent.updateCrumbs(myEditor.getCaretModel().getLogicalPosition());
}
@Override
public boolean canEat(final Update update) {
return true;
}
}
}