Files
openide/plugins/dev/intellij.dev.psiViewer/src/PsiViewerDialog.java
Alexandr Trushev 4dcc88eb48 IJPL-797 intellij.platform.editor review internal API
GitOrigin-RevId: 008ab023d706e3aa8dfe71ac1d5205f04fb8e71d
2024-06-11 20:04:46 +00:00

1191 lines
47 KiB
Java

// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.dev.psiViewer;
import com.intellij.CommonBundle;
import com.intellij.codeInsight.documentation.render.DocRenderManager;
import com.intellij.dev.psiViewer.formatter.BlockViewerPsiBasedTree;
import com.intellij.dev.psiViewer.properties.PsiViewerPropertiesTab;
import com.intellij.dev.psiViewer.properties.PsiViewerPropertiesTabViewModel;
import com.intellij.dev.psiViewer.stubs.StubViewerPsiBasedTree;
import com.intellij.ide.util.treeView.IndexComparator;
import com.intellij.ide.util.treeView.NodeRenderer;
import com.intellij.lang.ASTNode;
import com.intellij.lang.Language;
import com.intellij.lang.LanguageUtil;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.DataProvider;
import com.intellij.openapi.actionSystem.PlatformCoreDataKeys;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityKt;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.client.ClientSystemInfo;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.colors.EditorColors;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.event.*;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.highlighter.EditorHighlighter;
import com.intellij.openapi.editor.highlighter.EditorHighlighterFactory;
import com.intellij.openapi.editor.markup.*;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileTypes.FileNameMatcher;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.FileTypeManager;
import com.intellij.openapi.fileTypes.LanguageFileType;
import com.intellij.openapi.ide.CopyPasteManager;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.ComboBox;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.psi.*;
import com.intellij.psi.impl.DebugUtil;
import com.intellij.psi.impl.source.resolve.FileContextUtil;
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtilBase;
import com.intellij.psi.search.FilenameIndex;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.testFramework.LightVirtualFile;
import com.intellij.ui.*;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBList;
import com.intellij.ui.tabs.JBEditorTabsBase;
import com.intellij.ui.tabs.JBTabs;
import com.intellij.ui.tabs.JBTabsFactory;
import com.intellij.ui.tabs.TabInfo;
import com.intellij.ui.tree.AsyncTreeModel;
import com.intellij.ui.tree.StructureTreeModel;
import com.intellij.ui.treeStructure.Tree;
import com.intellij.util.*;
import com.intellij.util.concurrency.AppExecutorUtil;
import com.intellij.util.containers.JBTreeTraverser;
import com.intellij.util.indexing.DumbModeAccessType;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.tree.TreeUtil;
import kotlinx.coroutines.CoroutineScope;
import kotlinx.coroutines.CoroutineScopeKt;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreePath;
import java.awt.*;
import java.awt.datatransfer.StringSelection;
import java.awt.event.*;
import java.util.List;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.regex.Pattern;
import static com.intellij.openapi.wm.IdeFocusManager.getGlobalInstance;
/**
* @author Konstantin Bulenkov
*/
public class PsiViewerDialog extends DialogWrapper implements DataProvider {
private static final Color BOX_COLOR = new JBColor(new Color(0xFC6C00), new Color(0xDE6C01));
public static final Logger LOG = Logger.getInstance(PsiViewerDialog.class);
private final Project myProject;
private final StructureTreeModel<ViewerTreeStructure> myStructureTreeModel;
private final ViewerTreeStructure myTreeStructure;
private JPanel myPanel;
private ComboBox<PsiViewerSourceWrapper> myFileTypeComboBox;
private JCheckBox myShowWhiteSpacesBox;
private JCheckBox myShowTreeNodesCheckBox;
private JCheckBox myShowEmptyPropertiesCheckBox;
private JCheckBox myUpdatePsiTreeCheckbox;
private JBLabel myDialectLabel;
private JComboBox<Language> myDialectComboBox;
private JLabel myExtensionLabel;
private JComboBox<String> myExtensionComboBox;
private JPanel myTextPanel;
private JSplitPane myTextSplit;
private JSplitPane myTreeSplit;
private final JPanel myPsiTreePanel;
private final Tree myPsiTree;
private final JList<String> myRefs;
private TitledSeparator myTextSeparator;
private final TitledSeparator myPsiTreeSeparator;
@NotNull
private final StubViewerPsiBasedTree myStubTree;
@NotNull
private final CoroutineScope myCoroutineScope;
@NotNull
final PsiViewerPropertiesTabViewModel myPsiViewerPropertiesTabViewModel;
@NotNull
private final BlockViewerPsiBasedTree myBlockTree;
private RangeHighlighter myHighlighter;
private final Set<PsiViewerSourceWrapper> mySourceWrappers = new TreeSet<>();
private final EditorEx myEditor;
private final EditorListener myEditorListener = new EditorListener();
private String myLastParsedText;
private int myLastParsedTextHashCode = 17;
private int myNewDocumentHashCode = 11;
private final boolean myExternalDocument;
private final Map<PsiElement, PsiElement[]> myRefsResolvedCache = new HashMap<>();
private final PsiFile myOriginalPsiFile;
@NotNull
private final JBTabs myTabs;
@NotNull
private final SingleAlarm myPsiUpdateAlarm;
private static class ExtensionComparator implements Comparator<String> {
private final String myOnTop;
ExtensionComparator(String onTop) {
myOnTop = onTop;
}
@Override
public int compare(@NotNull String o1, @NotNull String o2) {
if (o1.equals(myOnTop)) return -1;
if (o2.equals(myOnTop)) return 1;
return o1.compareToIgnoreCase(o2);
}
}
public PsiViewerDialog(@NotNull Project project, @Nullable Editor selectedEditor) {
super(project, true, IdeModalityType.MODELESS);
myPsiTreePanel = new JPanel(new BorderLayout());
myPsiTreeSeparator = new TitledSeparator("P&SI Tree");
myPsiTree = new Tree();
myProject = project;
myExternalDocument = selectedEditor != null;
myOriginalPsiFile = getOriginalPsiFile(project, selectedEditor);
myTabs = createTabPanel(project);
myRefs = new JBList<>(new DefaultListModel<>());
myPsiUpdateAlarm = new SingleAlarm(() -> doUpdatePsi(), 1500, getDisposable(), Alarm.ThreadToUse.SWING_THREAD);
myCoroutineScope = CoroutineScopeKt.CoroutineScope(ModalityKt.asContextElement(ModalityState.nonModal()));
myPsiViewerPropertiesTabViewModel = new PsiViewerPropertiesTabViewModel(myProject, myCoroutineScope, PsiViewerSettings.getSettings(), (psiElement) -> {
focusTree();
selectElement(psiElement);
});
myTreeStructure = new ViewerTreeStructure(myProject);
myStructureTreeModel = new StructureTreeModel<>(myTreeStructure, IndexComparator.getInstance(), getDisposable());
AsyncTreeModel asyncTreeModel = new AsyncTreeModel(myStructureTreeModel, getDisposable());
myPsiTree.setModel(asyncTreeModel);
ViewerPsiBasedTree.PsiTreeUpdater psiTreeUpdater = new ViewerPsiBasedTree.PsiTreeUpdater() {
private final TextAttributes myAttributes;
{
myAttributes = new TextAttributes();
myAttributes.setEffectColor(BOX_COLOR);
myAttributes.setEffectType(EffectType.ROUNDED_BOX);
}
@Override
public void updatePsiTree(@NotNull PsiElement toSelect, @Nullable TextRange selectRangeInEditor) {
if (selectRangeInEditor != null) {
int start = selectRangeInEditor.getStartOffset();
int end = selectRangeInEditor.getEndOffset();
clearSelection();
if (end <= myEditor.getDocument().getTextLength()) {
myHighlighter = myEditor.getMarkupModel()
.addRangeHighlighter(start, end, HighlighterLayer.LAST, myAttributes, HighlighterTargetArea.EXACT_RANGE);
myEditor.getCaretModel().moveToOffset(start);
myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
}
}
updateReferences(toSelect);
if (!myPsiTree.hasFocus()) {
selectElement(toSelect);
}
}
};
myStubTree = new StubViewerPsiBasedTree(project, psiTreeUpdater);
myBlockTree = new BlockViewerPsiBasedTree(project, psiTreeUpdater);
Disposer.register(getDisposable(), myStubTree);
Disposer.register(getDisposable(), myBlockTree);
setOKButtonText(DevPsiViewerBundle.message("button.build.psi.tree"));
setCancelButtonText(DevPsiViewerBundle.message("button.close"));
Disposer.register(myProject, getDisposable());
VirtualFile selectedFile = selectedEditor == null ? null : FileDocumentManager.getInstance().getFile(selectedEditor.getDocument());
setTitle(selectedFile == null ?
DevPsiViewerBundle.message("dialog.title.psi.viewer") :
DevPsiViewerBundle.message("dialog.title.psi.viewer.with.file", selectedFile.getName()));
if (selectedEditor != null) {
myEditor = (EditorEx)EditorFactory.getInstance().createEditor(selectedEditor.getDocument(), myProject);
}
else {
PsiViewerSettings settings = PsiViewerSettings.getSettings();
Document document = EditorFactory.getInstance().createDocument(StringUtil.notNullize(settings.text));
myEditor = (EditorEx)EditorFactory.getInstance().createEditor(document, myProject);
myEditor.getSelectionModel().setSelection(0, document.getTextLength());
}
myEditor.getSettings().setLineMarkerAreaShown(false);
DocRenderManager.setDocRenderingEnabled(myEditor, false);
init();
if (selectedEditor != null) {
doOKAction();
ApplicationManager.getApplication().invokeLater(() -> {
getGlobalInstance().doWhenFocusSettlesDown(() -> getGlobalInstance().requestFocus(myEditor.getContentComponent(), true));
myEditor.getCaretModel().moveToOffset(selectedEditor.getCaretModel().getOffset());
myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
SelectionModel selectionModel = selectedEditor.getSelectionModel();
if (selectionModel.hasSelection()) {
myEditor.getSelectionModel().setSelection(selectionModel.getSelectionStart(), selectionModel.getSelectionEnd());
}
}, ModalityState.stateForComponent(myPanel));
}
}
private static @Nullable PsiFile getOriginalPsiFile(@NotNull Project project, @Nullable Editor selectedEditor) {
return selectedEditor != null ? PsiDocumentManager.getInstance(project).getPsiFile(selectedEditor.getDocument()) : null;
}
@NotNull
private JBTabs createTabPanel(@NotNull Project project) {
JBEditorTabsBase tabs = JBTabsFactory.createEditorTabs(project, getDisposable());
tabs.getPresentation().setAlphabeticalMode(false);
return tabs;
}
@Override
protected void init() {
initMnemonics();
initTree(myPsiTree);
TreeCellRenderer renderer = myPsiTree.getCellRenderer();
myPsiTree.setCellRenderer((tree, value, selected, expanded, leaf, row, hasFocus) -> {
Component c = renderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
if (value instanceof DefaultMutableTreeNode) {
Object userObject = ((DefaultMutableTreeNode)value).getUserObject();
if (userObject instanceof ViewerNodeDescriptor descriptor) {
Object element = descriptor.getElement();
if (c instanceof NodeRenderer nodeRenderer) {
nodeRenderer.setToolTipText(getElementDescription(element));
}
PsiElement fileContext = null;
if (element instanceof PsiElement psiElement) {
PsiFile containingFile = ReadAction.compute(psiElement::getContainingFile);
if (containingFile != null) fileContext = FileContextUtil.getFileContext(containingFile);
}
boolean isInjected = (element instanceof ViewerTreeStructure.Inject) || fileContext != null;
if (isInjected) {
TextAttributes attr =
EditorColorsManager.getInstance().getGlobalScheme().getAttributes(EditorColors.INJECTED_LANGUAGE_FRAGMENT);
c.setBackground(attr.getBackgroundColor());
}
}
}
return c;
});
myPsiTree.addTreeSelectionListener(new MyPsiTreeSelectionListener());
myPsiTreePanel.add(myPsiTreeSeparator, BorderLayout.NORTH);
JScrollPane scrollPane = ScrollPaneFactory.createScrollPane(myPsiTree, true);
myPsiTreePanel.add(scrollPane, BorderLayout.CENTER);
myTreeSplit.add(myPsiTreePanel, JSplitPane.LEFT);
myTreeSplit.add(myTabs.getComponent(), JSplitPane.RIGHT);
JPanel referencesPanel = new JPanel(new BorderLayout());
referencesPanel.add(myRefs);
var propertiesTab = new PsiViewerPropertiesTab(myPsiViewerPropertiesTabViewModel, myCoroutineScope);
var propertiesTabInfo = new TabInfo(propertiesTab.getComponent()).setText(DevPsiViewerBundle.message("tab.title.properties"));
myTabs.addTab(propertiesTabInfo);
myTabs.addTab(new TabInfo(myRefs).setText(DevPsiViewerBundle.message("tab.title.references")));
myTabs.addTab(new TabInfo(myBlockTree.getComponent()).setText(DevPsiViewerBundle.message("tab.title.block.structure")));
myTabs.addTab(new TabInfo(myStubTree.getComponent()).setText(DevPsiViewerBundle.message("tab.title.stub.structure")));
PsiViewerSettings settings = PsiViewerSettings.getSettings();
myTabs.setSelectionChangeHandler((tab, focus, el) -> {
settings.lastSelectedTabIndex = myTabs.getIndexOf(tab);
if (tab == propertiesTabInfo) {
myPsiViewerPropertiesTabViewModel.openTab();
} else {
myPsiViewerPropertiesTabViewModel.closeTab();
}
return el.run();
});
int tabIndex = settings.lastSelectedTabIndex;
TabInfo defaultInfo = tabIndex < myTabs.getTabCount() ? myTabs.getTabAt(tabIndex) : null;
if (defaultInfo != null) {
myTabs.select(defaultInfo, false);
}
GoToListener listener = new GoToListener();
myRefs.addKeyListener(listener);
myRefs.addMouseListener(listener);
myRefs.getSelectionModel().addListSelectionListener(listener);
myRefs.setCellRenderer(new DefaultListCellRenderer() {
@Override
public Component getListCellRendererComponent(@NotNull JList list,
Object value,
int index,
boolean isSelected,
boolean cellHasFocus) {
Component comp = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
PsiElement[] elements = myRefsResolvedCache.get(getPsiElement());
if (elements == null || elements.length <= index || elements[index] == null) {
comp.setForeground(JBColor.RED);
}
return comp;
}
});
myEditor.getSettings().setFoldingOutlineShown(false);
myEditor.getDocument().addDocumentListener(myEditorListener, getDisposable());
myEditor.getSelectionModel().addSelectionListener(myEditorListener);
myEditor.getCaretModel().addCaretListener(myEditorListener);
FocusTraversalPolicy oldPolicy = getPeer().getWindow().getFocusTraversalPolicy();
getPeer().getWindow().setFocusTraversalPolicy(new LayoutFocusTraversalPolicy() {
@Override
public Component getInitialComponent(@NotNull Window window) {
return myEditor.getComponent();
}
});
Disposer.register(getDisposable(), () -> getPeer().getWindow().setFocusTraversalPolicy(oldPolicy));
VirtualFile file = myExternalDocument ? FileDocumentManager.getInstance().getFile(myEditor.getDocument()) : null;
Language curLanguage = LanguageUtil.getLanguageForPsi(myProject, file);
String type = curLanguage != null ? curLanguage.getDisplayName() : settings.type;
PsiViewerSourceWrapper lastUsed = null;
mySourceWrappers.addAll(PsiViewerSourceWrapper.getExtensionBasedWrappers());
List<PsiViewerSourceWrapper> fileTypeBasedWrappers = PsiViewerSourceWrapper.getFileTypeBasedWrappers();
for (PsiViewerSourceWrapper wrapper : fileTypeBasedWrappers) {
mySourceWrappers.addAll(fileTypeBasedWrappers);
if (lastUsed == null && wrapper.getText().equals(type) ||
curLanguage != null && wrapper.myFileType == curLanguage.getAssociatedFileType()) {
lastUsed = wrapper;
}
}
myFileTypeComboBox.setSwingPopup(false);
myFileTypeComboBox.setModel(new CollectionComboBoxModel<>(new ArrayList<>(mySourceWrappers), lastUsed));
myFileTypeComboBox.setRenderer(SimpleListCellRenderer.create((label, value, index) -> {
if (value != null) {
label.setText(value.getText());
label.setIcon(value.getIcon());
}
}));
ComboboxSpeedSearch search1 = new ComboboxSpeedSearch(myFileTypeComboBox, null) {
@Override
protected String getElementText(Object element) {
return element instanceof PsiViewerSourceWrapper ? ((PsiViewerSourceWrapper)element).getText() : null;
}
};
search1.setupListeners();
myFileTypeComboBox.addActionListener(__ -> {
updateDialectsCombo(null);
updateExtensionsCombo();
updateEditor();
});
myDialectComboBox.addActionListener(__ -> updateEditor());
ComboboxSpeedSearch search = new ComboboxSpeedSearch(myDialectComboBox, null) {
@Override
protected String getElementText(Object element) {
return element instanceof Language ? ((Language)element).getDisplayName() : "<default>";
}
};
search.setupListeners();
myFileTypeComboBox.addFocusListener(new AutoExpandFocusListener(myFileTypeComboBox));
if (!myExternalDocument && lastUsed == null && !mySourceWrappers.isEmpty()) {
myFileTypeComboBox.setSelectedIndex(0);
}
myDialectComboBox.setRenderer(SimpleListCellRenderer.create(DevPsiViewerBundle.message("label.none"), value -> value.getDisplayName()));
myDialectComboBox.addFocusListener(new AutoExpandFocusListener(myDialectComboBox));
myExtensionComboBox.setRenderer(SimpleListCellRenderer.create("", value -> "." + value)); //NON-NLS
myExtensionComboBox.addFocusListener(new AutoExpandFocusListener(myExtensionComboBox));
myShowWhiteSpacesBox.addActionListener(__ -> {
myTreeStructure.setShowWhiteSpaces(myShowWhiteSpacesBox.isSelected());
myStructureTreeModel.invalidateAsync();
});
myShowTreeNodesCheckBox.addActionListener(__ -> {
myTreeStructure.setShowTreeNodes(myShowTreeNodesCheckBox.isSelected());
myStructureTreeModel.invalidateAsync();
});
myShowEmptyPropertiesCheckBox.addActionListener(__ -> {
myPsiViewerPropertiesTabViewModel.setShowEmptyProperties(myShowEmptyPropertiesCheckBox.isSelected());
});
myUpdatePsiTreeCheckbox.addActionListener(__ -> {
var isSelected = myUpdatePsiTreeCheckbox.isSelected();
settings.updatePsiTreeOnChanges = isSelected;
if (isSelected) {
queueUpdatePsi();
}
});
myShowWhiteSpacesBox.setSelected(settings.showWhiteSpaces);
myTreeStructure.setShowWhiteSpaces(settings.showWhiteSpaces);
myShowTreeNodesCheckBox.setSelected(settings.showTreeNodes);
myTreeStructure.setShowTreeNodes(settings.showTreeNodes);
myShowEmptyPropertiesCheckBox.setSelected(myPsiViewerPropertiesTabViewModel.getShowEmptyProperties());
myUpdatePsiTreeCheckbox.setSelected(settings.updatePsiTreeOnChanges);
myTextPanel.setLayout(new BorderLayout());
myTextPanel.add(myEditor.getComponent(), BorderLayout.CENTER);
updateDialectsCombo(settings.dialect);
updateExtensionsCombo();
registerCustomKeyboardActions();
Dimension size = DimensionService.getInstance().getSize(getDimensionServiceKey(), myProject);
if (size == null) {
DimensionService.getInstance().setSize(getDimensionServiceKey(), JBUI.size(800, 600), myProject);
}
myTextSplit.setDividerLocation(settings.textDividerLocation);
myTreeSplit.setDividerLocation(settings.treeDividerLocation);
updateEditor();
//GuiUtils.replaceJSplitPaneWithIDEASplitter(myTreeSplit, true);
GuiUtils.replaceJSplitPaneWithIDEASplitter(myTextSplit, true);
super.init();
}
@NotNull
private static @NlsSafe String getElementDescription(Object element) {
return element.getClass().getName();
}
public static void initTree(JTree tree) {
tree.setRootVisible(false);
tree.setShowsRootHandles(true);
tree.updateUI();
ToolTipManager.sharedInstance().registerComponent(tree);
TreeUtil.installActions(tree);
TreeUIHelper.getInstance().installTreeSpeedSearch(tree);
}
@Override
@NotNull
protected String getDimensionServiceKey() {
return "#com.intellij.internal.psiView.PsiViewerDialog";
}
@Override
protected String getHelpId() {
return "reference.psi.viewer";
}
@Override
public JComponent getPreferredFocusedComponent() {
return myEditor.getContentComponent();
}
private void registerCustomKeyboardActions() {
int mask = ClientSystemInfo.isMac() ? InputEvent.META_DOWN_MASK : InputEvent.ALT_DOWN_MASK;
registerKeyboardAction(__ -> focusEditor(), KeyStroke.getKeyStroke(KeyEvent.VK_T, mask));
registerKeyboardAction(__ -> focusTree(), KeyStroke.getKeyStroke(KeyEvent.VK_S, mask));
registerKeyboardAction(__ -> myBlockTree.focusTree(), KeyStroke.getKeyStroke(KeyEvent.VK_K, mask));
registerKeyboardAction(__ -> focusRefs(), KeyStroke.getKeyStroke(KeyEvent.VK_R, mask));
registerKeyboardAction(__ -> {
if (myRefs.isFocusOwner()) {
myBlockTree.focusTree();
}
else if (myPsiTree.isFocusOwner()) {
focusRefs();
}
else if (myBlockTree.isFocusOwner()) {
focusTree();
}
}, KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0));
}
private void registerKeyboardAction(ActionListener actionListener, KeyStroke keyStroke) {
getRootPane().registerKeyboardAction(actionListener, keyStroke, JComponent.WHEN_IN_FOCUSED_WINDOW);
}
private void focusEditor() {
IdeFocusManager.getInstance(myProject).requestFocus(myEditor.getContentComponent(), true);
}
private void focusTree() {
IdeFocusManager.getInstance(myProject).requestFocus(myPsiTree, true);
}
private void focusRefs() {
IdeFocusManager.getInstance(myProject).requestFocus(myRefs, true);
if (myRefs.getModel().getSize() > 0) {
if (myRefs.getSelectedIndex() == -1) {
myRefs.setSelectedIndex(0);
}
}
}
private void initMnemonics() {
myTextSeparator.setLabelFor(myEditor.getContentComponent());
myPsiTreeSeparator.setLabelFor(myPsiTree);
}
@Nullable
private PsiElement getPsiElement() {
TreePath path = myPsiTree.getSelectionPath();
return path == null ? null : getPsiElement((DefaultMutableTreeNode)path.getLastPathComponent());
}
@Nullable
private static PsiElement getPsiElement(DefaultMutableTreeNode node) {
if (node.getUserObject() instanceof ViewerNodeDescriptor descriptor) {
Object elementObject = descriptor.getElement();
return elementObject instanceof PsiElement
? (PsiElement)elementObject
: elementObject instanceof ASTNode ? ((ASTNode)elementObject).getPsi() : null;
}
return null;
}
private void updateDialectsCombo(@Nullable String lastUsed) {
Object source = getSource();
ArrayList<Language> items = new ArrayList<>();
if (source instanceof LanguageFileType) {
Language baseLang = ((LanguageFileType)source).getLanguage();
JBTreeTraverser.from(Language::getDialects).withRoot(baseLang)
.preOrderDfsTraversal()
.addAllTo(items);
items.subList(1, items.size()).sort(LanguageUtil.LANGUAGE_COMPARATOR);
}
myDialectComboBox.setModel(new CollectionComboBoxModel<>(items));
boolean visible = items.size() > 1;
myDialectLabel.setVisible(visible);
myDialectComboBox.setVisible(visible);
if (visible && (myExternalDocument || lastUsed != null)) {
VirtualFile file = myExternalDocument ? FileDocumentManager.getInstance().getFile(myEditor.getDocument()) : null;
Language curLanguage = LanguageUtil.getLanguageForPsi(myProject, file);
int idx = items.indexOf(curLanguage);
myDialectComboBox.setSelectedIndex(Math.max(idx, 0));
}
}
private void updateExtensionsCombo() {
Object source = getSource();
if (source instanceof LanguageFileType) {
List<String> extensions = getAllExtensions((LanguageFileType)source);
if (extensions.size() > 1) {
ExtensionComparator comp = new ExtensionComparator(extensions.get(0));
extensions.sort(comp);
SortedComboBoxModel<String> model = new SortedComboBoxModel<>(comp);
model.setAll(extensions);
myExtensionComboBox.setModel(model);
myExtensionComboBox.setVisible(true);
myExtensionLabel.setVisible(true);
VirtualFile file = myExternalDocument ? FileDocumentManager.getInstance().getFile(myEditor.getDocument()) : null;
String fileExt = file == null ? "" : FileUtilRt.getExtension(file.getName()); //NON-NLS
if (!fileExt.isEmpty() && extensions.contains(fileExt)) {
myExtensionComboBox.setSelectedItem(fileExt);
return;
}
myExtensionComboBox.setSelectedIndex(0);
return;
}
}
myExtensionComboBox.setVisible(false);
myExtensionLabel.setVisible(false);
}
private static final Pattern EXT_PATTERN = Pattern.compile("[a-z\\d]*");
private static List<String> getAllExtensions(LanguageFileType fileType) {
List<FileNameMatcher> associations = FileTypeManager.getInstance().getAssociations(fileType);
List<String> extensions = new ArrayList<>();
extensions.add(StringUtil.toLowerCase(fileType.getDefaultExtension()));
for (FileNameMatcher matcher : associations) {
String presentableString = StringUtil.toLowerCase(matcher.getPresentableString());
if (presentableString.startsWith("*.")) {
String ext = presentableString.substring(2);
if (!ext.isEmpty() && !extensions.contains(ext) && EXT_PATTERN.matcher(ext).matches()) {
extensions.add(ext);
}
}
}
return extensions;
}
@Override
protected JComponent createCenterPanel() {
return myPanel;
}
@Nullable
private Object getSource() {
PsiViewerSourceWrapper wrapper = (PsiViewerSourceWrapper)myFileTypeComboBox.getSelectedItem();
if (wrapper != null) {
return wrapper.myFileType != null ? wrapper.myFileType : wrapper.myExtension;
}
return null;
}
@Override
protected Action @NotNull [] createActions() {
AbstractAction copyPsi = new AbstractAction(DevPsiViewerBundle.message("cop.y.psi")) {
@Override
public void actionPerformed(@NotNull ActionEvent e) {
PsiElement element = parseText(myEditor.getDocument().getText());
setOriginalFiles(element);
List<PsiElement> allToParse = new ArrayList<>();
if (element instanceof PsiFile) {
allToParse.addAll(((PsiFile)element).getViewProvider().getAllFiles());
}
else if (element != null) {
allToParse.add(element);
}
StringBuilder data = new StringBuilder();
for (PsiElement psiElement : allToParse) {
data.append(DebugUtil.psiToString(psiElement, myShowWhiteSpacesBox.isSelected(), true));
}
CopyPasteManager.getInstance().setContents(new StringSelection(data.toString()));
}
};
return ArrayUtil.mergeArrays(new Action[]{copyPsi}, super.createActions());
}
@Override
protected void doOKAction() {
doUpdatePsi();
focusTree();
}
private void doUpdatePsi() {
String text = myEditor.getDocument().getText();
myEditor.getSelectionModel().removeSelection();
myLastParsedText = text;
myLastParsedTextHashCode = text.hashCode();
myNewDocumentHashCode = myLastParsedTextHashCode;
PsiElement rootElement = parseText(text);
setOriginalFiles(rootElement);
myTreeStructure.setRootPsiElement(rootElement);
myStructureTreeModel.invalidateAsync();
myPsiTree.setRootVisible(true);
myPsiTree.expandRow(0);
myPsiTree.setRootVisible(false);
myBlockTree.reloadTree(rootElement, text);
myStubTree.reloadTree(rootElement, text);
myRefsResolvedCache.clear();
myPsiViewerPropertiesTabViewModel.reset();
}
private void queueUpdatePsi() {
if (PsiViewerSettings.getSettings().updatePsiTreeOnChanges) {
myPsiUpdateAlarm.cancelAndRequest();
}
}
private PsiElement parseText(@NotNull String text) {
Object source = getSource();
try {
if (source instanceof PsiViewerExtension) {
return ((PsiViewerExtension)source).createElement(myProject, text);
}
if (source instanceof FileType type) {
String ext = type.getDefaultExtension();
if (myExtensionComboBox.isVisible() && myExtensionComboBox.getSelectedItem() != null) {
ext = StringUtil.toLowerCase(myExtensionComboBox.getSelectedItem().toString());
}
if (type instanceof LanguageFileType) {
Language dialect = (Language)myDialectComboBox.getSelectedItem();
if (dialect != null) {
return PsiFileFactory.getInstance(myProject).createFileFromText("Dummy." + ext, dialect, text);
}
}
return PsiFileFactory.getInstance(myProject).createFileFromText("Dummy." + ext, type, text);
}
}
catch (IncorrectOperationException e) {
Messages.showMessageDialog(myProject, e.getMessage(), CommonBundle.message("title.error"), Messages.getErrorIcon());
}
return null;
}
private void setOriginalFiles(@Nullable PsiElement root) {
if (root != null && myOriginalPsiFile != null) {
PsiFile newPsiFile = root.getContainingFile();
newPsiFile.putUserData(PsiFileFactory.ORIGINAL_FILE, myOriginalPsiFile);
VirtualFile newVirtualFile = newPsiFile.getVirtualFile();
if (newVirtualFile instanceof LightVirtualFile) {
((LightVirtualFile)newVirtualFile).setOriginalFile(myOriginalPsiFile.getVirtualFile());
}
}
}
@Override
public Object getData(@NotNull @NonNls String dataId) {
if (PlatformCoreDataKeys.BGT_DATA_PROVIDER.is(dataId)) {
Object selection =
myPsiTree.hasFocus() ? TreeUtil.getLastUserObject(myPsiTree.getSelectionPath()) :
myRefs.hasFocus() ? myRefs.getSelectedValue() : null;
return selection == null ? null : (DataProvider)slowId -> getSlowData(slowId, selection);
}
return null;
}
@Nullable
private PsiFile getSlowData(@NonNls String dataId, @NotNull Object selection) {
if (CommonDataKeys.NAVIGATABLE.is(dataId)) {
String fqn;
if (selection instanceof ViewerNodeDescriptor descriptor) {
Object elementObject = descriptor.getElement();
PsiElement element =
elementObject instanceof PsiElement ? (PsiElement)elementObject :
elementObject instanceof ASTNode ? ((ASTNode)elementObject).getPsi() : null;
fqn = element != null ? element.getClass().getName() : null;
}
else if (selection instanceof String str) {
fqn = str;
}
else {
fqn = null;
}
return fqn == null ? null : getContainingFileForClass(fqn);
}
return null;
}
private class MyPsiTreeSelectionListener implements TreeSelectionListener {
private final TextAttributes myAttributes;
MyPsiTreeSelectionListener() {
myAttributes = new TextAttributes();
myAttributes.setEffectColor(BOX_COLOR);
myAttributes.setEffectType(EffectType.ROUNDED_BOX);
}
@Override
public void valueChanged(@NotNull TreeSelectionEvent e) {
if (!myEditor.getDocument().getText().equals(myLastParsedText) || myBlockTree.isFocusOwner()) return;
TreePath path = myPsiTree.getSelectionPath();
clearSelection();
if (path != null) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
if (!(node.getUserObject() instanceof ViewerNodeDescriptor descriptor)) return;
Object elementObject = descriptor.getElement();
PsiElement element = elementObject instanceof PsiElement
? (PsiElement)elementObject
: elementObject instanceof ASTNode ? ((ASTNode)elementObject).getPsi() : null;
if (element != null) {
TextRange rangeInHostFile = InjectedLanguageManager.getInstance(myProject).injectedToHost(element, element.getTextRange());
int start = rangeInHostFile.getStartOffset();
int end = rangeInHostFile.getEndOffset();
PsiElement rootPsiElement = myTreeStructure.getRootPsiElement();
if (rootPsiElement != null) {
int baseOffset = rootPsiElement.getTextRange().getStartOffset();
start -= baseOffset;
end -= baseOffset;
}
int textLength = myEditor.getDocument().getTextLength();
if (end <= textLength) {
myHighlighter = myEditor.getMarkupModel()
.addRangeHighlighter(start, end, HighlighterLayer.LAST, myAttributes, HighlighterTargetArea.EXACT_RANGE);
if (myPsiTree.hasFocus()) {
myEditor.getCaretModel().moveToOffset(start);
myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
}
}
if (myPsiTree.hasFocus()) {
myBlockTree.selectNodeFromPsi(element);
myStubTree.selectNodeFromPsi(element);
}
myPsiViewerPropertiesTabViewModel.setSelectedPsiElement(element);
updateReferences(element);
}
}
}
}
private void updateReferences(@NotNull PsiElement element) {
DefaultListModel<String> model = (DefaultListModel<String>)myRefs.getModel();
model.clear();
String progressTitle = DevPsiViewerBundle.message("psi.viewer.progress.dialog.update.refs");
Callable<List<PsiReference>> updater =
() -> DumbModeAccessType.RELIABLE_DATA_ONLY.ignoreDumbMode(() -> doUpdateReferences(element));
List<PsiReference> psiReferences = computeSlowOperationsSafeInBgThread(myProject, progressTitle, updater);
for (PsiReference reference : psiReferences) {
model.addElement(getElementDescription(reference));
}
}
private @NotNull List<PsiReference> doUpdateReferences(@NotNull PsiElement element) {
PsiReferenceService referenceService = PsiReferenceService.getService();
List<PsiReference> psiReferences = referenceService.getReferences(element, PsiReferenceService.Hints.NO_HINTS);
if (myRefsResolvedCache.containsKey(element)) return psiReferences;
PsiElement[] cache = new PsiElement[psiReferences.size()];
for (int i = 0; i < psiReferences.size(); i++) {
PsiReference reference = psiReferences.get(i);
PsiElement resolveResult;
if (reference instanceof PsiPolyVariantReference) {
ResolveResult[] results = ((PsiPolyVariantReference)reference).multiResolve(true);
resolveResult = results.length == 0 ? null : results[0].getElement();
}
else {
resolveResult = reference.resolve();
}
cache[i] = resolveResult;
}
myRefsResolvedCache.put(element, cache);
return psiReferences;
}
private void clearSelection() {
if (myHighlighter != null) {
myEditor.getMarkupModel().removeHighlighter(myHighlighter);
myHighlighter.dispose();
}
}
@Override
public void doCancelAction() {
super.doCancelAction();
PsiViewerSettings settings = PsiViewerSettings.getSettings();
PsiViewerSourceWrapper wrapper = (PsiViewerSourceWrapper)myFileTypeComboBox.getSelectedItem();
if (wrapper != null) settings.type = wrapper.getText();
if (!myExternalDocument) {
settings.text = StringUtil.first(myEditor.getDocument().getText(), 2048, true);
}
settings.showTreeNodes = myShowTreeNodesCheckBox.isSelected();
settings.showWhiteSpaces = myShowWhiteSpacesBox.isSelected();
Object selectedDialect = myDialectComboBox.getSelectedItem();
settings.dialect = myDialectComboBox.isVisible() && selectedDialect != null ? selectedDialect.toString() : "";
settings.textDividerLocation = myTextSplit.getDividerLocation();
settings.treeDividerLocation = myTreeSplit.getDividerLocation();
}
@Override
public void dispose() {
if (!myEditor.isDisposed()) {
EditorFactory.getInstance().releaseEditor(myEditor);
}
CoroutineScopeKt.cancel(myCoroutineScope, null);
super.dispose();
}
@Nullable
private PsiFile getContainingFileForClass(@NotNull String fqn) {
String filename = fqn;
if (fqn.contains(".")) {
filename = fqn.substring(fqn.lastIndexOf('.') + 1);
}
if (filename.contains("$")) {
filename = filename.substring(0, filename.indexOf('$'));
}
filename += ".java";
PsiFile[] files = FilenameIndex.getFilesByName(myProject, filename, GlobalSearchScope.allScope(myProject));
return ArrayUtil.getFirstElement(files);
}
private class GoToListener implements KeyListener, MouseListener, ListSelectionListener {
private RangeHighlighter myListenerHighlighter;
private void navigate() {
String fqn = myRefs.getSelectedValue();
PsiFile file = getContainingFileForClass(fqn);
if (file != null) file.navigate(true);
}
@Override
public void keyPressed(@NotNull KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
navigate();
}
}
@Override
public void mouseClicked(@NotNull MouseEvent e) {
if (e.getClickCount() > 1) {
navigate();
}
}
@Override
public void valueChanged(@NotNull ListSelectionEvent e) {
clearSelection();
updateDialectsCombo(null);
updateExtensionsCombo();
int ind = myRefs.getSelectedIndex();
PsiElement element = getPsiElement();
if (ind > -1 && element != null) {
PsiReference[] references = element.getReferences();
if (ind < references.length) {
TextRange textRange = references[ind].getRangeInElement();
TextRange range = InjectedLanguageManager.getInstance(myProject).injectedToHost(element, element.getTextRange());
int start = range.getStartOffset();
PsiElement rootPsiElement = myTreeStructure.getRootPsiElement();
if (rootPsiElement != null) {
int baseOffset = rootPsiElement.getTextRange().getStartOffset();
start -= baseOffset;
}
start += textRange.getStartOffset();
int end = start + textRange.getLength();
//todo[kb] probably move highlight color to the editor color scheme?
TextAttributes highlightReferenceTextRange = new TextAttributes(null, null,
JBColor.namedColor("PsiViewer.referenceHighlightColor", 0xA8C023),
EffectType.BOLD_DOTTED_LINE, Font.PLAIN);
myListenerHighlighter = myEditor.getMarkupModel()
.addRangeHighlighter(start, end, HighlighterLayer.LAST,
highlightReferenceTextRange, HighlighterTargetArea.EXACT_RANGE);
}
}
}
public void clearSelection() {
if (myListenerHighlighter != null &&
ArrayUtil.contains(myListenerHighlighter, (Object[])myEditor.getMarkupModel().getAllHighlighters())) {
myListenerHighlighter.dispose();
myListenerHighlighter = null;
}
}
@Override
public void keyTyped(@NotNull KeyEvent e) {}
@Override
public void keyReleased(KeyEvent e) {}
@Override
public void mousePressed(@NotNull MouseEvent e) {}
@Override
public void mouseReleased(@NotNull MouseEvent e) {}
@Override
public void mouseEntered(@NotNull MouseEvent e) {}
@Override
public void mouseExited(@NotNull MouseEvent e) {}
}
private void updateEditor() {
Object source = getSource();
String fileName = "Dummy." + (source instanceof FileType ? ((FileType)source).getDefaultExtension() : "txt");
LightVirtualFile lightFile;
if (source instanceof PsiViewerExtension) {
lightFile = new LightVirtualFile(fileName, ((PsiViewerExtension)source).getDefaultFileType(), "");
}
else if (source instanceof LanguageFileType) {
lightFile = new LightVirtualFile(fileName, ObjectUtils
.chooseNotNull((Language)myDialectComboBox.getSelectedItem(), ((LanguageFileType)source).getLanguage()), "");
}
else if (source instanceof FileType) {
lightFile = new LightVirtualFile(fileName, (FileType)source, "");
}
else {
return;
}
EditorHighlighter highlighter = EditorHighlighterFactory.getInstance().createEditorHighlighter(myProject, lightFile);
try {
myEditor.setHighlighter(highlighter);
}
catch (Throwable e) {
LOG.warn(e);
}
}
private void selectElement(@NotNull PsiElement element) {
ReadAction
.nonBlocking(() -> getElementToChooseInPsiTree(element))
.finishOnUiThread(ModalityState.nonModal(), elementToChoose -> {
myStructureTreeModel.select(elementToChoose, myPsiTree, path -> {});
})
.expireWith(getDisposable())
.submit(AppExecutorUtil.getAppExecutorService());
}
private @NotNull PsiElement getElementToChooseInPsiTree(@NotNull PsiElement element) {
if (element.getFirstChild() != null) {
return element;
}
var parent = element.getParent();
if (parent == null) {
return element;
}
var isIdentifierName = element.toString().toLowerCase().contains("identifier"); // heuristic
if (isIdentifierName) {
return parent;
}
if (parent instanceof PsiNameIdentifierOwner parentAsNameIdentifierOwner) {
var parentNameIdentifier = parentAsNameIdentifierOwner.getNameIdentifier();
if (Objects.equals(parentNameIdentifier, element)) {
return parent;
}
}
return element;
}
private class EditorListener implements SelectionListener, DocumentListener, CaretListener {
@Override
public void caretPositionChanged(@NotNull CaretEvent e) {
if (!available() || myEditor.getSelectionModel().hasSelection()) return;
PsiElement rootPsiElement = myTreeStructure.getRootPsiElement();
if (rootPsiElement == null) return;
PsiElement rootElement = myTreeStructure.getRootPsiElement();
int baseOffset = rootPsiElement.getTextRange().getStartOffset();
int offset = myEditor.getCaretModel().getOffset() + baseOffset;
String progressDialogTitle = DevPsiViewerBundle.message("psi.viewer.progress.dialog.get.element.at.offset");
Callable<@Nullable PsiElement> finder = () -> InjectedLanguageUtilBase.findElementAtNoCommit(rootElement.getContainingFile(), offset);
PsiElement element = computeSlowOperationsSafeInBgThread(myProject, progressDialogTitle, finder);
if (element != null) {
myBlockTree.selectNodeFromEditor(element);
myStubTree.selectNodeFromEditor(element);
selectElement(element);
}
}
@Override
public void selectionChanged(@NotNull SelectionEvent e) {
if (!available() || !myEditor.getSelectionModel().hasSelection()) return;
PsiElement rootElement = myTreeStructure.getRootPsiElement();
if (rootElement == null) return;
SelectionModel selection = myEditor.getSelectionModel();
TextRange textRange = rootElement.getTextRange();
int baseOffset = textRange != null ? textRange.getStartOffset() : 0;
int start = selection.getSelectionStart() + baseOffset;
int end = selection.getSelectionEnd() + baseOffset - 1;
String progressDialogTitle = DevPsiViewerBundle.message("psi.viewer.progress.dialog.get.common.parent");
Callable<PsiElement> finder =
() -> findCommonParent(InjectedLanguageUtilBase.findElementAtNoCommit(rootElement.getContainingFile(), start),
InjectedLanguageUtilBase.findElementAtNoCommit(rootElement.getContainingFile(), end));
PsiElement element = computeSlowOperationsSafeInBgThread(myProject, progressDialogTitle, finder);
if (element != null) {
if (myEditor.getContentComponent().hasFocus()) {
myBlockTree.selectNodeFromEditor(element);
myStubTree.selectNodeFromEditor(element);
}
selectElement(element);
}
}
@Nullable
private static PsiElement findCommonParent(PsiElement start, PsiElement end) {
if (end == null || start == end) {
return start;
}
TextRange endRange = end.getTextRange();
PsiElement parent = start.getContext();
while (parent != null && !parent.getTextRange().contains(endRange)) {
parent = parent.getContext();
}
return parent;
}
private boolean available() {
return myLastParsedTextHashCode == myNewDocumentHashCode && myEditor.getContentComponent().hasFocus();
}
@Override
public void documentChanged(@NotNull DocumentEvent event) {
var oldDocumentHashCode = myNewDocumentHashCode;
var currentDocumentHashCode = event.getDocument().getText().hashCode();
myNewDocumentHashCode = currentDocumentHashCode;
if (oldDocumentHashCode != currentDocumentHashCode) {
queueUpdatePsi();
}
}
}
private static final class AutoExpandFocusListener extends FocusAdapter {
private final JComboBox<?> myComboBox;
private final Component myParent;
private AutoExpandFocusListener(JComboBox<?> comboBox) {
myComboBox = comboBox;
myParent = UIUtil.findUltimateParent(myComboBox);
}
@Override
public void focusGained(@NotNull FocusEvent e) {
Component from = e.getOppositeComponent();
if (!e.isTemporary() && from != null && !myComboBox.isPopupVisible() && isUnder(from, myParent)) {
myComboBox.setPopupVisible(true);
}
}
private static boolean isUnder(@NotNull Component component, Component parent) {
while (component != null) {
if (component == parent) return true;
component = component.getParent();
}
return false;
}
}
private static <T> T computeSlowOperationsSafeInBgThread(@NotNull Project project,
@NlsContexts.DialogTitle @NotNull String progressDialogTitle,
@NotNull Callable<T> callable) {
return ProgressManager.getInstance().run(new Task.WithResult<>(project, progressDialogTitle, true) {
@Override
protected T compute(@NotNull ProgressIndicator indicator) throws RuntimeException {
return ReadAction.nonBlocking(callable).executeSynchronously();
}
});
}
}