blob: b83d832f8348f281d118a4814225bac1e20f3f51 [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.hint;
import com.intellij.codeInsight.CodeInsightBundle;
import com.intellij.find.FindUtil;
import com.intellij.icons.AllIcons;
import com.intellij.ide.highlighter.HighlighterFactory;
import com.intellij.navigation.ItemPresentation;
import com.intellij.navigation.NavigationItem;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.highlighter.EditorHighlighter;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.fileEditor.FileEditorProvider;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx;
import com.intellij.openapi.fileEditor.ex.FileEditorProviderManager;
import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.ComboBox;
import com.intellij.openapi.ui.popup.JBPopup;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.vcs.FileStatusManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.ui.IdeBorderFactory;
import com.intellij.ui.ListCellRendererWrapper;
import com.intellij.ui.ScreenUtil;
import com.intellij.ui.SideBorder;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.usages.UsageView;
import com.intellij.util.DocumentUtil;
import com.intellij.util.PairFunction;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import javax.swing.*;
import javax.swing.border.CompoundBorder;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.*;
import java.util.List;
public class ImplementationViewComponent extends JPanel {
@NonNls private static final String TEXT_PAGE_KEY = "Text";
@NonNls private static final String BINARY_PAGE_KEY = "Binary";
private static final Logger LOG = Logger.getInstance("#" + ImplementationViewComponent.class.getName());
private PsiElement[] myElements;
private int myIndex;
private final Editor myEditor;
private final JPanel myViewingPanel;
private final JLabel myLocationLabel;
private final JLabel myCountLabel;
private final CardLayout myBinarySwitch;
private final JPanel myBinaryPanel;
private ComboBox myFileChooser;
private FileEditor myNonTextEditor;
private FileEditorProvider myCurrentNonTextEditorProvider;
private JBPopup myHint;
private String myTitle;
private final ActionToolbar myToolbar;
private JLabel myLabel;
public void setHint(final JBPopup hint, @NotNull String title) {
myHint = hint;
myTitle = title;
}
public boolean hasElementsToShow() {
return myElements != null && myElements.length > 0;
}
private static class FileDescriptor {
public final PsiFile myFile;
public final String myElementPresentation;
public FileDescriptor(PsiFile file, PsiElement element) {
myFile = file;
myElementPresentation = getPresentation(element);
}
@Nullable
private static String getPresentation(PsiElement element) {
if (element instanceof NavigationItem) {
final ItemPresentation presentation = ((NavigationItem)element).getPresentation();
if (presentation != null) {
return presentation.getPresentableText();
}
}
if (element instanceof PsiNamedElement) return ((PsiNamedElement)element).getName();
return null;
}
public String getPresentableName(VirtualFile vFile) {
final String presentableName = vFile.getPresentableName();
if (myElementPresentation == null) {
return presentableName;
}
if (Comparing.strEqual(vFile.getName(), myElementPresentation + "." + vFile.getExtension())){
return presentableName;
}
return presentableName + " (" + myElementPresentation + ")";
}
}
public ImplementationViewComponent(PsiElement[] elements, final int index) {
super(new BorderLayout());
final Project project = elements.length > 0 ? elements[0].getProject() : null;
EditorFactory factory = EditorFactory.getInstance();
Document doc = factory.createDocument("");
doc.setReadOnly(true);
myEditor = factory.createEditor(doc, project);
((EditorEx)myEditor).setBackgroundColor(EditorFragmentComponent.getBackgroundColor(myEditor));
final EditorSettings settings = myEditor.getSettings();
settings.setAdditionalLinesCount(1);
settings.setAdditionalColumnsCount(1);
settings.setLineMarkerAreaShown(false);
settings.setIndentGuidesShown(false);
settings.setLineNumbersShown(false);
settings.setFoldingOutlineShown(false);
myBinarySwitch = new CardLayout();
myViewingPanel = new JPanel(myBinarySwitch);
myEditor.setBorder(null);
((EditorEx)myEditor).getScrollPane().setViewportBorder(JBScrollPane.createIndentBorder());
myViewingPanel.add(myEditor.getComponent(), TEXT_PAGE_KEY);
myBinaryPanel = new JPanel(new BorderLayout());
myViewingPanel.add(myBinaryPanel, BINARY_PAGE_KEY);
add(myViewingPanel, BorderLayout.CENTER);
myToolbar = createToolbar();
myLocationLabel = new JLabel();
myCountLabel = new JLabel();
final JPanel header = new JPanel(new BorderLayout(2, 0));
header.setBorder(BorderFactory.createCompoundBorder(IdeBorderFactory.createBorder(SideBorder.BOTTOM),
IdeBorderFactory.createEmptyBorder(0, 0, 0, 5)));
final JPanel toolbarPanel = new JPanel(new GridBagLayout());
final GridBagConstraints gc = new GridBagConstraints(GridBagConstraints.RELATIVE, 0, 1, 1, 0, 0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(0,2,0,0), 0,0);
toolbarPanel.add(myToolbar.getComponent(), gc);
setPreferredSize(new Dimension(600, 400));
update(elements, new PairFunction<PsiElement[], List<FileDescriptor>, Boolean>() {
@Override
public Boolean fun(final PsiElement[] psiElements, final List<FileDescriptor> fileDescriptors) {
if (psiElements.length == 0) return false;
myElements = psiElements;
myIndex = index < myElements.length ? index : 0;
PsiFile psiFile = getContainingFile(myElements[myIndex]);
VirtualFile virtualFile = psiFile.getVirtualFile();
EditorHighlighter highlighter;
if (virtualFile != null)
highlighter = HighlighterFactory.createHighlighter(project, virtualFile);
else {
String fileName = psiFile.getName(); // some artificial psi file, lets do best we can
highlighter = HighlighterFactory.createHighlighter(project, fileName);
}
((EditorEx)myEditor).setHighlighter(highlighter);
gc.fill = GridBagConstraints.HORIZONTAL;
gc.weightx = 1;
myLabel = new JLabel();
myFileChooser = new ComboBox(fileDescriptors.toArray(new FileDescriptor[fileDescriptors.size()]), 250);
myFileChooser.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
int index = myFileChooser.getSelectedIndex();
if (myIndex != index) {
myIndex = index;
updateControls();
}
}
});
toolbarPanel.add(myFileChooser, gc);
if (myElements.length > 1) {
updateRenderer(project);
myLabel.setVisible(false);
}
else {
myFileChooser.setVisible(false);
myCountLabel.setVisible(false);
VirtualFile file = psiFile.getVirtualFile();
if (file != null) {
myLabel.setIcon(getIconForFile(psiFile));
myLabel.setForeground(FileStatusManager.getInstance(project).getStatus(file).getColor());
myLabel.setText(file.getPresentableName());
myLabel.setBorder(new CompoundBorder(IdeBorderFactory.createRoundedBorder(), IdeBorderFactory.createEmptyBorder(0, 0, 0, 5)));
}
toolbarPanel.add(myLabel, gc);
}
gc.fill = GridBagConstraints.NONE;
gc.weightx = 0;
toolbarPanel.add(myCountLabel, gc);
header.add(toolbarPanel, BorderLayout.CENTER);
header.add(myLocationLabel, BorderLayout.EAST);
add(header, BorderLayout.NORTH);
updateControls();
return true;
}
});
}
private void updateRenderer(final Project project) {
myFileChooser.setRenderer(new ListCellRendererWrapper<FileDescriptor>() {
@Override
public void customize(JList list, FileDescriptor value, int index, boolean selected, boolean hasFocus) {
final PsiFile file = value.myFile;
setIcon(getIconForFile(file));
final VirtualFile vFile = file.getVirtualFile();
setForeground(FileStatusManager.getInstance(project).getStatus(vFile).getColor());
//noinspection ConstantConditions
setText(value.getPresentableName(vFile));
}
});
}
@TestOnly
public String[] getVisibleFiles() {
final ComboBoxModel model = myFileChooser.getModel();
String[] result = new String[model.getSize()];
for (int i = 0; i < model.getSize(); i++) {
FileDescriptor o = (FileDescriptor)model.getElementAt(i);
result[i] = o.getPresentableName(o.myFile.getVirtualFile());
}
return result;
}
public void update(@NotNull final PsiElement[] elements, final int index) {
update(elements, new PairFunction<PsiElement[], List<FileDescriptor>, Boolean>() {
@Override
public Boolean fun(PsiElement[] psiElements, List<FileDescriptor> fileDescriptors) {
if (psiElements.length == 0) return false;
final Project project = psiElements[0].getProject();
myElements = psiElements;
myIndex = index < myElements.length ? index : 0;
PsiFile psiFile = getContainingFile(myElements[myIndex]);
VirtualFile virtualFile = psiFile.getVirtualFile();
EditorHighlighter highlighter;
if (virtualFile != null)
highlighter = HighlighterFactory.createHighlighter(project, virtualFile);
else {
String fileName = psiFile.getName(); // some artificial psi file, lets do best we can
highlighter = HighlighterFactory.createHighlighter(project, fileName);
}
((EditorEx)myEditor).setHighlighter(highlighter);
if (myElements.length > 1) {
myFileChooser.setVisible(true);
myCountLabel.setVisible(true);
myLabel.setVisible(false);
myFileChooser.setModel(new DefaultComboBoxModel(fileDescriptors.toArray(new FileDescriptor[fileDescriptors.size()])));
updateRenderer(project);
}
else {
myFileChooser.setVisible(false);
myCountLabel.setVisible(false);
VirtualFile file = psiFile.getVirtualFile();
if (file != null) {
myLabel.setIcon(getIconForFile(psiFile));
myLabel.setForeground(FileStatusManager.getInstance(project).getStatus(file).getColor());
myLabel.setText(file.getPresentableName());
myLabel.setBorder(new CompoundBorder(IdeBorderFactory.createRoundedBorder(), IdeBorderFactory.createEmptyBorder(0, 0, 0, 5)));
myLabel.setVisible(true);
}
}
updateControls();
revalidate();
repaint();
return true;
}
});
}
private static void update(@NotNull PsiElement[] elements, @NotNull PairFunction<PsiElement[], List<FileDescriptor>, Boolean> fun) {
List<PsiElement> candidates = new ArrayList<PsiElement>(elements.length);
List<FileDescriptor> files = new ArrayList<FileDescriptor>(elements.length);
final Set<String> names = new HashSet<String>();
for (PsiElement element : elements) {
if (element instanceof PsiNamedElement) {
names.add(((PsiNamedElement)element).getName());
}
}
for (PsiElement element : elements) {
PsiFile file = getContainingFile(element);
if (file == null) continue;
final PsiElement parent = element.getParent();
files.add(new FileDescriptor(file, names.size() > 1 || parent == file ? element : parent));
candidates.add(element);
}
fun.fun(PsiUtilCore.toPsiElementArray(candidates), files);
}
private static Icon getIconForFile(PsiFile psiFile) {
return psiFile.getNavigationElement().getIcon(0);
}
public JComponent getPreferredFocusableComponent() {
return myElements.length > 1 ? myFileChooser : myEditor.getContentComponent();
}
private void updateControls() {
updateLabels();
updateCombo();
updateEditorText();
myToolbar.updateActionsImmediately();
}
private void updateCombo() {
if (myFileChooser != null && myFileChooser.isVisible()) {
myFileChooser.setSelectedIndex(myIndex);
}
}
private void updateEditorText() {
disposeNonTextEditor();
final PsiElement foundElement = myElements[myIndex];
final PsiElement elt = foundElement.getNavigationElement();
LOG.assertTrue(elt != null, foundElement);
final Project project = foundElement.getProject();
final PsiFile psiFile = getContainingFile(elt);
final VirtualFile vFile = psiFile != null ? psiFile.getVirtualFile() : null;
if (vFile == null) return;
final FileEditorProvider[] providers = FileEditorProviderManager.getInstance().getProviders(project, vFile);
for (FileEditorProvider provider : providers) {
if (provider instanceof TextEditorProvider) {
updateTextElement(elt);
myBinarySwitch.show(myViewingPanel, TEXT_PAGE_KEY);
break;
}
else if (provider.accept(project, vFile)) {
myCurrentNonTextEditorProvider = provider;
myNonTextEditor = myCurrentNonTextEditorProvider.createEditor(project, vFile);
myBinaryPanel.removeAll();
myBinaryPanel.add(myNonTextEditor.getComponent());
myBinarySwitch.show(myViewingPanel, BINARY_PAGE_KEY);
break;
}
}
}
private void disposeNonTextEditor() {
if (myNonTextEditor != null) {
myCurrentNonTextEditorProvider.disposeEditor(myNonTextEditor);
myNonTextEditor = null;
myCurrentNonTextEditorProvider = null;
}
}
private void updateTextElement(final PsiElement elt) {
final String newText = getNewText(elt);
if (newText == null || Comparing.strEqual(newText, myEditor.getDocument().getText())) return;
DocumentUtil.writeInRunUndoTransparentAction(new Runnable() {
@Override
public void run() {
Document fragmentDoc = myEditor.getDocument();
fragmentDoc.setReadOnly(false);
fragmentDoc.replaceString(0, fragmentDoc.getTextLength(), newText);
fragmentDoc.setReadOnly(true);
myEditor.getCaretModel().moveToOffset(0);
myEditor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
}
});
}
@Nullable
public static String getNewText(PsiElement elt) {
Project project = elt.getProject();
PsiFile psiFile = getContainingFile(elt);
final Document doc = PsiDocumentManager.getInstance(project).getDocument(psiFile);
if (doc == null) return null;
if (elt.getTextRange() == null) {
return null;
}
final ImplementationTextSelectioner implementationTextSelectioner =
LanguageImplementationTextSelectioner.INSTANCE.forLanguage(elt.getLanguage());
int start = implementationTextSelectioner.getTextStartOffset(elt);
final int end = implementationTextSelectioner.getTextEndOffset(elt);
final int lineStart = doc.getLineStartOffset(doc.getLineNumber(start));
final int lineEnd = end < doc.getTextLength() ? doc.getLineEndOffset(doc.getLineNumber(end)) : doc.getTextLength();
return doc.getCharsSequence().subSequence(lineStart, lineEnd).toString();
}
private static PsiFile getContainingFile(final PsiElement elt) {
PsiFile psiFile = elt.getContainingFile();
if (psiFile == null) return null;
return psiFile.getOriginalFile();
}
@Override
public void removeNotify() {
super.removeNotify();
if (!ScreenUtil.isStandardAddRemoveNotify(this))
return;
EditorFactory.getInstance().releaseEditor(myEditor);
disposeNonTextEditor();
}
private void updateLabels() {
//TODO: Move from JavaDoc to somewhere more appropriate place.
ElementLocationUtil.customizeElementLabel(myElements[myIndex], myLocationLabel);
//noinspection AutoBoxing
myCountLabel.setText(CodeInsightBundle.message("n.of.m", myIndex + 1, myElements.length));
}
private ActionToolbar createToolbar() {
DefaultActionGroup group = new DefaultActionGroup();
BackAction back = new BackAction();
back.registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0)), this);
group.add(back);
ForwardAction forward = new ForwardAction();
forward.registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0)), this);
group.add(forward);
EditSourceActionBase edit = new EditSourceAction();
edit.registerCustomShortcutSet(new CompositeShortcutSet(CommonShortcuts.getEditSource(), CommonShortcuts.ENTER), this);
group.add(edit);
edit = new ShowSourceAction();
edit.registerCustomShortcutSet(new CompositeShortcutSet(CommonShortcuts.getViewSource(), CommonShortcuts.CTRL_ENTER), this);
group.add(edit);
return ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, group, true);
}
private void goBack() {
myIndex--;
updateControls();
}
private void goForward() {
myIndex++;
updateControls();
}
public int getIndex() {
return myIndex;
}
public PsiElement[] getElements() {
return myElements;
}
public UsageView showInUsageView() {
return FindUtil.showInUsageView(null, collectNonBinaryElements(), myTitle, myEditor.getProject());
}
private class BackAction extends AnAction implements HintManagerImpl.ActionToIgnore {
public BackAction() {
super(CodeInsightBundle.message("quick.definition.back"), null, AllIcons.Actions.Back);
}
@Override
public void actionPerformed(AnActionEvent e) {
goBack();
}
@Override
public void update(AnActionEvent e) {
Presentation presentation = e.getPresentation();
presentation.setEnabled(myIndex > 0);
}
}
private class ForwardAction extends AnAction implements HintManagerImpl.ActionToIgnore {
public ForwardAction() {
super(CodeInsightBundle.message("quick.definition.forward"), null, AllIcons.Actions.Forward);
}
@Override
public void actionPerformed(AnActionEvent e) {
goForward();
}
@Override
public void update(AnActionEvent e) {
Presentation presentation = e.getPresentation();
presentation.setEnabled(myElements != null && myIndex < myElements.length - 1);
}
}
private class EditSourceAction extends EditSourceActionBase {
public EditSourceAction() {
super(true, AllIcons.Actions.EditSource, CodeInsightBundle.message("quick.definition.edit.source"));
}
@Override public void actionPerformed(AnActionEvent e) {
super.actionPerformed(e);
if (myHint.isVisible()) {
myHint.cancel();
}
}
}
private class ShowSourceAction extends EditSourceActionBase implements HintManagerImpl.ActionToIgnore {
public ShowSourceAction() {
super(false, AllIcons.Actions.Preview, CodeInsightBundle.message("quick.definition.show.source"));
}
}
private class EditSourceActionBase extends AnAction {
private final boolean myFocusEditor;
public EditSourceActionBase(boolean focusEditor, Icon icon, String text) {
super(text, null, icon);
myFocusEditor = focusEditor;
}
@Override
public void update(AnActionEvent e) {
e.getPresentation().setEnabled(myFileChooser == null || !myFileChooser.isPopupVisible());
}
@Override
public void actionPerformed(AnActionEvent e) {
PsiElement element = myElements[myIndex];
PsiElement navigationElement = element.getNavigationElement();
PsiFile file = getContainingFile(navigationElement);
if (file == null) return;
VirtualFile virtualFile = file.getVirtualFile();
if (virtualFile == null) return;
Project project = element.getProject();
FileEditorManagerEx fileEditorManager = FileEditorManagerEx.getInstanceEx(project);
OpenFileDescriptor descriptor = new OpenFileDescriptor(project, virtualFile, navigationElement.getTextOffset());
fileEditorManager.openTextEditor(descriptor, myFocusEditor);
}
}
private PsiElement[] collectNonBinaryElements() {
List<PsiElement> result = new ArrayList<PsiElement>();
for (PsiElement element : myElements) {
if (!(element instanceof PsiBinaryFile)) {
result.add(element);
}
}
return PsiUtilCore.toPsiElementArray(result);
}
}