/* * Copyright 2000-2015 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.util.text.StringUtil; import com.intellij.openapi.vcs.FileStatusManager; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; import com.intellij.psi.util.PsiTreeUtil; 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 com.intellij.util.ui.JBUI; 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.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; 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); private PsiElement[] myElements; private int myIndex; private final Editor myEditor; private volatile boolean myEditorReleased; 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; private final String myLocationString; public FileDescriptor(PsiFile file, PsiElement element) { myFile = file; final ItemPresentation presentation = element instanceof NavigationItem ? ((NavigationItem)element).getPresentation() : null; if (presentation != null) { myElementPresentation = presentation.getPresentableText(); myLocationString = presentation.getLocationString(); } else { myElementPresentation = element instanceof PsiNamedElement ? ((PsiNamedElement)element).getName() : null; myLocationString = 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 + (!StringUtil.isEmptyOrSpaces(myLocationString) ? " " + myLocationString : ""); } 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(JBUI.size(600, 400)); update(elements, (psiElements, 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 index1 = myFileChooser.getSelectedIndex(); if (myIndex != index1) { myIndex = index1; 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, (psiElements, fileDescriptors) -> { if (myEditor.isDisposed()) return false; 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<>(elements.length); List<FileDescriptor> files = new ArrayList<>(elements.length); final Set<String> names = new HashSet<>(); for (PsiElement element : elements) { if (element instanceof PsiNamedElement) { names.add(((PsiNamedElement)element).getName()); } if (names.size() > 1) { break; } } for (PsiElement element : elements) { PsiFile file = getContainingFile(element); if (file == null) continue; if (names.size() > 1) { files.add(new FileDescriptor(file, element)); } else { final PsiElement parent = PsiTreeUtil.getStubOrPsiParent(element); if (parent == file) { files.add(new FileDescriptor(file, element)); } else { files.add(new FileDescriptor(file, 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(() -> { 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); int end = implementationTextSelectioner.getTextEndOffset(elt); CharSequence rawDefinition = doc.getCharsSequence().subSequence(start, end); while (end > start && StringUtil.isLineBreak(rawDefinition.charAt(end - start - 1))) { // removing trailing EOLs from definition end--; } 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) && !myEditorReleased) { myEditorReleased = true; // remove notify can be called several times for popup windows 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<>(); for (PsiElement element : myElements) { if (!(element instanceof PsiBinaryFile)) { result.add(element); } } return PsiUtilCore.toPsiElementArray(result); } }