blob: cd1ec63d1879b2ab4c196913af283ea8fe203bbd [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.internal.psiView;
import com.intellij.diagnostic.AttachmentFactory;
import com.intellij.diagnostic.LogMessageEx;
import com.intellij.formatting.ASTBlock;
import com.intellij.formatting.Block;
import com.intellij.formatting.FormattingModel;
import com.intellij.formatting.FormattingModelBuilder;
import com.intellij.ide.util.treeView.AbstractTreeStructure;
import com.intellij.ide.util.treeView.NodeRenderer;
import com.intellij.internal.psiView.formattingblocks.BlockTreeBuilder;
import com.intellij.internal.psiView.formattingblocks.BlockTreeNode;
import com.intellij.internal.psiView.formattingblocks.BlockTreeStructure;
import com.intellij.lang.ASTNode;
import com.intellij.lang.Language;
import com.intellij.lang.LanguageFormatting;
import com.intellij.lang.LanguageUtil;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.DataProvider;
import com.intellij.openapi.application.AccessToken;
import com.intellij.openapi.application.ApplicationManager;
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.EditorHighlighterFactory;
import com.intellij.openapi.editor.markup.*;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.fileTypes.*;
import com.intellij.openapi.fileTypes.impl.AbstractFileType;
import com.intellij.openapi.ide.CopyPasteManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.DimensionService;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiFileFactory;
import com.intellij.psi.PsiReference;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.intellij.psi.impl.DebugUtil;
import com.intellij.psi.impl.source.resolve.FileContextUtil;
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
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.treeStructure.Tree;
import com.intellij.util.ArrayUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.tree.TreeUtil;
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.*;
import java.awt.*;
import java.awt.datatransfer.StringSelection;
import java.awt.event.*;
import java.util.*;
import java.util.List;
import java.util.regex.Pattern;
/**
* @author Konstantin Bulenkov
*/
public class PsiViewerDialog extends DialogWrapper implements DataProvider, Disposable {
private static final String REFS_CACHE = "References Resolve Cache";
private static final Color BOX_COLOR = new JBColor(new Color(0xFC6C00), new Color(0xDE6C01));
private static final Logger LOG = Logger.getInstance("#com.intellij.internal.psiView.PsiViewerDialog");
private final Project myProject;
private JPanel myPanel;
private JComboBox myFileTypeComboBox;
private JCheckBox myShowWhiteSpacesBox;
private JCheckBox myShowTreeNodesCheckBox;
private JBLabel myDialectLabel;
private JComboBox myDialectComboBox;
private JLabel myExtensionLabel;
private JComboBox myExtensionComboBox;
private JPanel myTextPanel;
private JPanel myStructureTreePanel;
private JPanel myReferencesPanel;
private JSplitPane myTextSplit;
private JSplitPane myTreeSplit;
private Tree myPsiTree;
private ViewerTreeBuilder myPsiTreeBuilder;
private JList myRefs;
private Tree myBlockTree;
private JPanel myBlockStructurePanel;
private JSplitPane myBlockRefSplitPane;
private JCheckBox myShowBlocksCheckBox;
private TitledSeparator myTextSeparator;
private TitledSeparator myPsiTreeSeparator;
private TitledSeparator myRefsSeparator;
private TitledSeparator myBlockTreeSeparator;
@Nullable
private BlockTreeBuilder myBlockTreeBuilder;
private RangeHighlighter myHighlighter;
private HashMap<PsiElement, BlockTreeNode> myPsiToBlockMap;
private final Set<SourceWrapper> mySourceWrappers = ContainerUtil.newTreeSet();
private final EditorEx myEditor;
private final EditorListener myEditorListener = new EditorListener();
private String myLastParsedText = null;
private int myLastParsedTextHashCode = 17;
private int myNewDocumentHashCode = 11;
private int myIgnoreBlockTreeSelectionMarker = 0;
private PsiFile myCurrentFile;
private String myInitText;
private String myFileType;
private void createUIComponents() {
myPsiTree = new Tree(new DefaultTreeModel(new DefaultMutableTreeNode()));
myBlockTree = new Tree(new DefaultTreeModel(new DefaultMutableTreeNode()));
myRefs = new JBList(new DefaultListModel());
}
private static class ExtensionComparator implements Comparator<String> {
private final String myOnTop;
public 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);
}
}
private static class SourceWrapper implements Comparable<SourceWrapper> {
private final FileType myFileType;
private final PsiViewerExtension myExtension;
public SourceWrapper(final FileType fileType) {
myFileType = fileType;
myExtension = null;
}
public SourceWrapper(final PsiViewerExtension extension) {
myFileType = null;
myExtension = extension;
}
public String getText() {
return myFileType != null ? myFileType.getName() + " file" : myExtension.getName();
}
@Nullable
public Icon getIcon() {
return myFileType != null ? myFileType.getIcon() : myExtension.getIcon();
}
@Override
public int compareTo(@NotNull final SourceWrapper o) {
return getText().compareToIgnoreCase(o.getText());
}
}
public PsiViewerDialog(Project project, boolean modal, @Nullable PsiFile currentFile, @Nullable Editor currentEditor) {
super(project, true);
myCurrentFile = currentFile;
myProject = project;
setModal(modal);
setOKButtonText("&Build PSI Tree");
setCancelButtonText("&Close");
Disposer.register(myProject, getDisposable());
EditorEx editor = null;
if (myCurrentFile == null) {
setTitle("PSI Viewer");
}
else {
setTitle("PSI Context Viewer: " + myCurrentFile.getName());
myFileType = myCurrentFile.getLanguage().getDisplayName();
if (currentEditor != null) {
myInitText = currentEditor.getSelectionModel().getSelectedText();
}
if (myInitText == null) {
myInitText = currentFile.getText();
editor = (EditorEx)EditorFactory.getInstance().createEditor(currentFile.getViewProvider().getDocument(), myProject);
}
}
if (editor == null) {
final Document document = EditorFactory.getInstance().createDocument("");
editor = (EditorEx)EditorFactory.getInstance().createEditor(document, myProject);
}
editor.getSettings().setLineMarkerAreaShown(false);
myEditor = editor;
init();
if (myCurrentFile != null) {
doOKAction();
}
}
@Override
protected void init() {
initMnemonics();
initTree(myPsiTree);
final TreeCellRenderer renderer = myPsiTree.getCellRenderer();
myPsiTree.setCellRenderer(new TreeCellRenderer() {
@Override
public Component getTreeCellRendererComponent(@NotNull JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
final Component c = renderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
if (value instanceof DefaultMutableTreeNode) {
final Object userObject = ((DefaultMutableTreeNode)value).getUserObject();
if (userObject instanceof ViewerNodeDescriptor) {
final Object element = ((ViewerNodeDescriptor)userObject).getElement();
if (c instanceof NodeRenderer) {
((NodeRenderer)c).setToolTipText(element == null ? null : element.getClass().getName());
}
if (element instanceof PsiElement && FileContextUtil.getFileContext(((PsiElement)element).getContainingFile()) != null ||
element instanceof ViewerTreeStructure.Inject) {
final TextAttributes attr = EditorColorsManager.getInstance().getGlobalScheme().getAttributes(EditorColors.INJECTED_LANGUAGE_FRAGMENT);
c.setBackground(attr.getBackgroundColor());
}
}
}
return c;
}
});
myPsiTreeBuilder = new ViewerTreeBuilder(myProject, myPsiTree);
Disposer.register(getDisposable(), myPsiTreeBuilder);
myPsiTree.addTreeSelectionListener(new MyPsiTreeSelectionListener());
final 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) {
final Component comp = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if (resolve(index) == null) {
comp.setForeground(JBColor.RED);
}
return comp;
}
});
initTree(myBlockTree);
myEditor.getSettings().setFoldingOutlineShown(false);
myEditor.getDocument().addDocumentListener(myEditorListener);
myEditor.getSelectionModel().addSelectionListener(myEditorListener);
myEditor.getCaretModel().addCaretListener(myEditorListener);
getPeer().getWindow().setFocusTraversalPolicy(new LayoutFocusTraversalPolicy() {
@Override
public Component getInitialComponent(@NotNull Window window) {
return myEditor.getComponent();
}
});
final PsiViewerSettings settings = PsiViewerSettings.getSettings();
final String type = myFileType != null ? myFileType : settings.type;
SourceWrapper lastUsed = null;
for (PsiViewerExtension extension : Extensions.getExtensions(PsiViewerExtension.EP_NAME)) {
final SourceWrapper wrapper = new SourceWrapper(extension);
mySourceWrappers.add(wrapper);
}
final Set<FileType> allFileTypes = ContainerUtil.newHashSet();
Collections.addAll(allFileTypes, FileTypeManager.getInstance().getRegisteredFileTypes());
for (Language language : Language.getRegisteredLanguages()) {
final FileType fileType = language.getAssociatedFileType();
if (fileType != null) {
allFileTypes.add(fileType);
}
}
Language curLanguage = myCurrentFile != null ? myCurrentFile.getLanguage() : null;
for (FileType fileType : allFileTypes) {
if (fileType != StdFileTypes.GUI_DESIGNER_FORM &&
fileType != StdFileTypes.IDEA_MODULE &&
fileType != StdFileTypes.IDEA_PROJECT &&
fileType != StdFileTypes.IDEA_WORKSPACE &&
fileType != FileTypes.ARCHIVE &&
fileType != FileTypes.UNKNOWN &&
fileType != FileTypes.PLAIN_TEXT &&
!(fileType instanceof AbstractFileType) &&
!fileType.isBinary() &&
!fileType.isReadOnly()) {
final SourceWrapper wrapper = new SourceWrapper(fileType);
mySourceWrappers.add(wrapper);
if (lastUsed == null && wrapper.getText().equals(type)) lastUsed = wrapper;
if (myCurrentFile != null && wrapper.myFileType instanceof LanguageFileType &&
wrapper.myFileType.equals(curLanguage.getAssociatedFileType())) {
lastUsed = wrapper;
}
}
}
myFileTypeComboBox.setModel(new CollectionComboBoxModel(ContainerUtil.newArrayList(mySourceWrappers), lastUsed));
myFileTypeComboBox.setRenderer(new ListCellRendererWrapper<SourceWrapper>() {
@Override
public void customize(JList list, SourceWrapper value, int index, boolean selected, boolean hasFocus) {
if (value != null) {
setText(value.getText());
setIcon(value.getIcon());
}
}
});
new ComboboxSpeedSearch(myFileTypeComboBox) {
@Override
protected String getElementText(Object element) {
return element instanceof SourceWrapper? ((SourceWrapper)element).getText() : null;
}
};
myFileTypeComboBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(@NotNull ActionEvent e) {
updateDialectsCombo(null);
updateExtensionsCombo();
updateEditor();
}
});
myDialectComboBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(@NotNull ActionEvent e) {
updateEditor();
}
});
myFileTypeComboBox.addFocusListener(new AutoExpandFocusListener(myFileTypeComboBox));
if (myCurrentFile == null && lastUsed == null && mySourceWrappers.size() > 0) {
myFileTypeComboBox.setSelectedIndex(0);
}
myDialectComboBox.setRenderer(new ListCellRendererWrapper<Language>() {
@Override
public void customize(final JList list, final Language value, final int index, final boolean selected, final boolean hasFocus) {
setText(value != null ? value.getDisplayName() : "<default>");
}
});
myDialectComboBox.addFocusListener(new AutoExpandFocusListener(myDialectComboBox));
myExtensionComboBox.setRenderer(new ListCellRendererWrapper<String>() {
@Override
public void customize(JList list, String value, int index, boolean selected, boolean hasFocus) {
if (value != null) setText("." + value);
}
});
myExtensionComboBox.addFocusListener(new AutoExpandFocusListener(myExtensionComboBox));
final ViewerTreeStructure psiTreeStructure = (ViewerTreeStructure)myPsiTreeBuilder.getTreeStructure();
myShowWhiteSpacesBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(@NotNull ActionEvent e) {
psiTreeStructure.setShowWhiteSpaces(myShowWhiteSpacesBox.isSelected());
myPsiTreeBuilder.queueUpdate();
}
});
myShowTreeNodesCheckBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(@NotNull ActionEvent e) {
psiTreeStructure.setShowTreeNodes(myShowTreeNodesCheckBox.isSelected());
myPsiTreeBuilder.queueUpdate();
}
});
myShowWhiteSpacesBox.setSelected(settings.showWhiteSpaces);
psiTreeStructure.setShowWhiteSpaces(settings.showWhiteSpaces);
myShowTreeNodesCheckBox.setSelected(settings.showTreeNodes);
psiTreeStructure.setShowTreeNodes(settings.showTreeNodes);
myShowBlocksCheckBox.setSelected(settings.showBlocks);
myBlockStructurePanel.setVisible(settings.showBlocks);
myShowBlocksCheckBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(@NotNull ActionEvent e) {
if (!myShowBlocksCheckBox.isSelected()) {
settings.blockRefDividerLocation = myBlockRefSplitPane.getDividerLocation();
}
else {
myBlockRefSplitPane.setDividerLocation(settings.blockRefDividerLocation);
}
myBlockStructurePanel.setVisible(myShowBlocksCheckBox.isSelected());
myBlockStructurePanel.repaint();
}
});
myTextPanel.setLayout(new BorderLayout());
myTextPanel.add(myEditor.getComponent(), BorderLayout.CENTER);
final String text = myCurrentFile == null ? settings.text : myInitText;
final AccessToken token = ApplicationManager.getApplication().acquireWriteActionLock(getClass());
try {
myEditor.getDocument().setText(text);
myEditor.getSelectionModel().setSelection(0, text.length());
}
finally {
token.finish();
}
updateDialectsCombo(settings.dialect);
updateExtensionsCombo();
registerCustomKeyboardActions();
final Dimension size = DimensionService.getInstance().getSize(getDimensionServiceKey(), myProject);
if (size == null) {
DimensionService.getInstance().setSize(getDimensionServiceKey(), new Dimension(800, 600));
}
myTextSplit.setDividerLocation(settings.textDividerLocation);
myTreeSplit.setDividerLocation(settings.treeDividerLocation);
myBlockRefSplitPane.setDividerLocation(settings.blockRefDividerLocation);
updateEditor();
super.init();
}
private static void initTree(JTree tree) {
UIUtil.setLineStyleAngled(tree);
tree.setRootVisible(false);
tree.setShowsRootHandles(true);
tree.updateUI();
ToolTipManager.sharedInstance().registerComponent(tree);
TreeUtil.installActions(tree);
new TreeSpeedSearch(tree);
}
@Override
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() {
final int mask = SystemInfo.isMac ? InputEvent.META_DOWN_MASK : InputEvent.ALT_DOWN_MASK;
registerKeyboardAction(new ActionListener() {
@Override
public void actionPerformed(@NotNull ActionEvent e) {
focusEditor();
}
}, KeyStroke.getKeyStroke(KeyEvent.VK_T, mask));
registerKeyboardAction(new ActionListener() {
@Override
public void actionPerformed(@NotNull ActionEvent e) {
focusTree();
}
}, KeyStroke.getKeyStroke(KeyEvent.VK_S, mask));
registerKeyboardAction(new ActionListener() {
@Override
public void actionPerformed(@NotNull ActionEvent e) {
focusBlockTree();
}
}, KeyStroke.getKeyStroke(KeyEvent.VK_K, mask));
registerKeyboardAction(new ActionListener() {
@Override
public void actionPerformed(@NotNull ActionEvent e) {
focusRefs();
}
}, KeyStroke.getKeyStroke(KeyEvent.VK_R, mask));
registerKeyboardAction(new ActionListener() {
@Override
public void actionPerformed(@NotNull ActionEvent e) {
if (myRefs.isFocusOwner()) {
focusBlockTree();
}
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 focusBlockTree() {
IdeFocusManager.getInstance(myProject).requestFocus(myBlockTree, true);
}
private void initMnemonics() {
myTextSeparator.setLabelFor(myEditor.getContentComponent());
myPsiTreeSeparator.setLabelFor(myPsiTree);
myRefsSeparator.setLabelFor(myRefs);
myBlockTreeSeparator.setLabelFor(myBlockTree);
}
@Nullable
private PsiElement getPsiElement() {
final TreePath path = myPsiTree.getSelectionPath();
return path == null ? null : getPsiElement((DefaultMutableTreeNode)path.getLastPathComponent());
}
@Nullable
private static PsiElement getPsiElement(DefaultMutableTreeNode node) {
if (node.getUserObject() instanceof ViewerNodeDescriptor) {
ViewerNodeDescriptor descriptor = (ViewerNodeDescriptor)node.getUserObject();
Object elementObject = descriptor.getElement();
return elementObject instanceof PsiElement
? (PsiElement)elementObject
: elementObject instanceof ASTNode ? ((ASTNode)elementObject).getPsi() : null;
}
return null;
}
private void updateDialectsCombo(@Nullable final String lastUsed) {
final Object source = getSource();
ArrayList<Language> items = new ArrayList<Language>();
if (source instanceof LanguageFileType) {
final Language baseLang = ((LanguageFileType)source).getLanguage();
items.add(baseLang);
Language[] dialects = LanguageUtil.getLanguageDialects(baseLang);
Arrays.sort(dialects, LanguageUtil.LANGUAGE_COMPARATOR);
items.addAll(Arrays.asList(dialects));
}
myDialectComboBox.setModel(new CollectionComboBoxModel(items));
final int size = items.size();
final boolean visible = size > 1;
myDialectLabel.setVisible(visible);
myDialectComboBox.setVisible(visible);
if (visible && (myCurrentFile != null || lastUsed != null)) {
String curLanguage = myCurrentFile != null ? myCurrentFile.getLanguage().toString() : lastUsed;
for (int i = 0; i < size; ++i) {
if (curLanguage.equals(items.get(i).toString())) {
myDialectComboBox.setSelectedIndex(i);
return;
}
}
myDialectComboBox.setSelectedIndex(size > 0 ? 0 : -1);
}
}
private void updateExtensionsCombo() {
final Object source = getSource();
if (source instanceof LanguageFileType) {
final List<String> extensions = getAllExtensions((LanguageFileType)source);
if (extensions.size() > 1) {
final ExtensionComparator comp = new ExtensionComparator(extensions.get(0));
Collections.sort(extensions, comp);
final SortedComboBoxModel<String> model = new SortedComboBoxModel<String>(comp);
model.setAll(extensions);
myExtensionComboBox.setModel(model);
myExtensionComboBox.setVisible(true);
myExtensionLabel.setVisible(true);
String fileExt = myCurrentFile != null ? FileUtilRt.getExtension(myCurrentFile.getName()) : "";
if (fileExt.length() > 0 && 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-z0-9]*");
private static List<String> getAllExtensions(LanguageFileType fileType) {
final List<FileNameMatcher> associations = FileTypeManager.getInstance().getAssociations(fileType);
final List<String> extensions = new ArrayList<String>();
extensions.add(fileType.getDefaultExtension().toLowerCase());
for (FileNameMatcher matcher : associations) {
final String presentableString = matcher.getPresentableString().toLowerCase();
if (presentableString.startsWith("*.")) {
final String ext = presentableString.substring(2);
if (ext.length() > 0 && !extensions.contains(ext) && EXT_PATTERN.matcher(ext).matches()) {
extensions.add(ext);
}
}
}
return extensions;
}
@Override
protected JComponent createCenterPanel() {
return myPanel;
}
@Nullable
private Object getSource() {
final SourceWrapper wrapper = (SourceWrapper)myFileTypeComboBox.getSelectedItem();
if (wrapper != null) {
return wrapper.myFileType != null ? wrapper.myFileType : wrapper.myExtension;
}
return null;
}
@NotNull
@Override
protected Action[] createActions() {
AbstractAction copyPsi = new AbstractAction("Cop&y PSI") {
@Override
public void actionPerformed(@NotNull ActionEvent e) {
PsiElement element = parseText(myEditor.getDocument().getText());
List<PsiElement> allToParse = new ArrayList<PsiElement>();
if (element instanceof PsiFile) {
allToParse.addAll(((PsiFile)element).getViewProvider().getAllFiles());
}
else if (element != null) {
allToParse.add(element);
}
String data = "";
for (PsiElement psiElement : allToParse) {
data += DebugUtil.psiToString(psiElement, !myShowWhiteSpacesBox.isSelected(), true);
}
CopyPasteManager.getInstance().setContents(new StringSelection(data));
}
};
return ArrayUtil.mergeArrays(new Action[]{copyPsi}, super.createActions());
}
@Override
protected void doOKAction() {
if (myBlockTreeBuilder != null) {
Disposer.dispose(myBlockTreeBuilder);
}
final String text = myEditor.getDocument().getText();
myEditor.getSelectionModel().removeSelection();
myLastParsedText = text;
myLastParsedTextHashCode = text.hashCode();
myNewDocumentHashCode = myLastParsedTextHashCode;
PsiElement rootElement = parseText(text);
focusTree();
ViewerTreeStructure structure = (ViewerTreeStructure)myPsiTreeBuilder.getTreeStructure();
structure.setRootPsiElement(rootElement);
myPsiTreeBuilder.queueUpdate();
myPsiTree.setRootVisible(true);
myPsiTree.expandRow(0);
myPsiTree.setRootVisible(false);
if (!myShowBlocksCheckBox.isSelected()) {
return;
}
Block rootBlock = rootElement == null ? null : buildBlocks(rootElement);
if (rootBlock == null) {
myBlockTreeBuilder = null;
myBlockTree.setRootVisible(false);
myBlockTree.setVisible(false);
return;
}
myBlockTree.setVisible(true);
BlockTreeStructure blockTreeStructure = new BlockTreeStructure();
BlockTreeNode rootNode = new BlockTreeNode(rootBlock, null);
blockTreeStructure.setRoot(rootNode);
myBlockTreeBuilder = new BlockTreeBuilder(myBlockTree, blockTreeStructure);
myPsiToBlockMap = new HashMap<PsiElement, BlockTreeNode>();
final PsiElement psiFile = ((ViewerTreeStructure)myPsiTreeBuilder.getTreeStructure()).getRootPsiElement();
initMap(rootNode, psiFile);
PsiElement rootPsi = rootNode.getBlock() instanceof ASTBlock ?
((ASTBlock)rootNode.getBlock()).getNode().getPsi() : rootElement;
BlockTreeNode blockNode = myPsiToBlockMap.get(rootPsi);
if (blockNode == null) {
LOG.error(LogMessageEx
.createEvent("PsiViewer: rootNode not found", "Current language: " + rootElement.getContainingFile().getLanguage(),
AttachmentFactory.createAttachment(rootElement.getContainingFile().getOriginalFile().getVirtualFile())));
blockNode = findBlockNode(rootPsi);
}
blockTreeStructure.setRoot(blockNode);
myBlockTree.addTreeSelectionListener(new MyBlockTreeSelectionListener());
myBlockTree.setRootVisible(true);
myBlockTree.expandRow(0);
myBlockTreeBuilder.queueUpdate();
}
private PsiElement parseText(String text) {
final Object source = getSource();
try {
if (source instanceof PsiViewerExtension) {
return ((PsiViewerExtension)source).createElement(myProject, text);
}
if (source instanceof FileType) {
final FileType type = (FileType)source;
String ext = type.getDefaultExtension();
if (myExtensionComboBox.isVisible()) {
ext = myExtensionComboBox.getSelectedItem().toString().toLowerCase();
}
if (type instanceof LanguageFileType) {
final Language language = ((LanguageFileType)type).getLanguage();
final Language dialect = (Language)myDialectComboBox.getSelectedItem();
return PsiFileFactory.getInstance(myProject).createFileFromText("Dummy." + ext, dialect == null ? language : dialect, text);
}
return PsiFileFactory.getInstance(myProject).createFileFromText("Dummy." + ext, type, text);
}
}
catch (IncorrectOperationException e) {
Messages.showMessageDialog(myProject, e.getMessage(), "Error", Messages.getErrorIcon());
}
return null;
}
@Nullable
private static Block buildBlocks(@NotNull PsiElement rootElement) {
FormattingModelBuilder formattingModelBuilder = LanguageFormatting.INSTANCE.forContext(rootElement);
CodeStyleSettings settings = CodeStyleSettingsManager.getSettings(rootElement.getProject());
if (formattingModelBuilder != null) {
FormattingModel formattingModel = formattingModelBuilder.createModel(rootElement, settings);
return formattingModel.getRootBlock();
}
else {
return null;
}
}
private void initMap(BlockTreeNode rootBlockNode, PsiElement psiEl) {
PsiElement currentElem = null;
if (rootBlockNode.getBlock() instanceof ASTBlock) {
ASTNode node = ((ASTBlock)rootBlockNode.getBlock()).getNode();
if (node != null) {
currentElem = node.getPsi();
}
}
if (currentElem == null) {
currentElem =
InjectedLanguageUtil
.findElementAtNoCommit(psiEl.getContainingFile(), rootBlockNode.getBlock().getTextRange().getStartOffset());
}
myPsiToBlockMap.put(currentElem, rootBlockNode);
//nested PSI elements with same ranges will be mapped to one blockNode
// assert currentElem != null; //for Scala-language plugin etc it can be null, because formatterBlocks is not instance of ASTBlock
TextRange curTextRange = currentElem.getTextRange();
PsiElement parentElem = currentElem.getParent();
while (parentElem != null && parentElem.getTextRange().equals(curTextRange)) {
myPsiToBlockMap.put(parentElem, rootBlockNode);
parentElem = parentElem.getParent();
}
for (BlockTreeNode block : rootBlockNode.getChildren()) {
initMap(block, psiEl);
}
}
@Override
public Object getData(@NonNls String dataId) {
if (CommonDataKeys.NAVIGATABLE.is(dataId)) {
String fqn = null;
if (myPsiTree.hasFocus()) {
final TreePath path = myPsiTree.getSelectionPath();
if (path != null) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
if (!(node.getUserObject() instanceof ViewerNodeDescriptor)) return null;
ViewerNodeDescriptor descriptor = (ViewerNodeDescriptor)node.getUserObject();
Object elementObject = descriptor.getElement();
final PsiElement element = elementObject instanceof PsiElement
? (PsiElement)elementObject
: elementObject instanceof ASTNode ? ((ASTNode)elementObject).getPsi() : null;
if (element != null) {
fqn = element.getClass().getName();
}
}
} else if (myRefs.hasFocus()) {
final Object value = myRefs.getSelectedValue();
if (value instanceof String) {
fqn = (String)value;
}
}
if (fqn != null) {
return getContainingFileForClass(fqn);
}
}
return null;
}
private class MyPsiTreeSelectionListener implements TreeSelectionListener {
private final TextAttributes myAttributes;
public 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.hasFocus()) return;
TreePath path = myPsiTree.getSelectionPath();
clearSelection();
if (path != null) {
DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
if (!(node.getUserObject() instanceof ViewerNodeDescriptor)) return;
ViewerNodeDescriptor descriptor = (ViewerNodeDescriptor)node.getUserObject();
Object elementObject = descriptor.getElement();
final 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();
final ViewerTreeStructure treeStructure = (ViewerTreeStructure)myPsiTreeBuilder.getTreeStructure();
PsiElement rootPsiElement = treeStructure.getRootPsiElement();
if (rootPsiElement != null) {
int baseOffset = rootPsiElement.getTextRange().getStartOffset();
start -= baseOffset;
end -= baseOffset;
}
final 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 (myBlockTreeBuilder != null && myPsiTree.hasFocus()) {
BlockTreeNode currentBlockNode = findBlockNode(element);
if (currentBlockNode != null) {
selectBlockNode(currentBlockNode);
}
}
updateReferences(element);
}
}
}
}
@Nullable
private BlockTreeNode findBlockNode(PsiElement element) {
BlockTreeNode result = myPsiToBlockMap.get(element);
if (result == null) {
TextRange rangeInHostFile = InjectedLanguageManager.getInstance(myProject).injectedToHost(element, element.getTextRange());
result = findBlockNode(rangeInHostFile, true);
}
return result;
}
private void selectBlockNode(@Nullable BlockTreeNode currentBlockNode) {
if (myBlockTreeBuilder == null) return;
if (currentBlockNode != null) {
myIgnoreBlockTreeSelectionMarker++;
myBlockTreeBuilder.select(currentBlockNode, new Runnable() {
@Override
public void run() {
// hope this is always called!
assert myIgnoreBlockTreeSelectionMarker > 0;
myIgnoreBlockTreeSelectionMarker--;
}
});
}
else {
myIgnoreBlockTreeSelectionMarker++;
try {
myBlockTree.getSelectionModel().clearSelection();
}
finally {
assert myIgnoreBlockTreeSelectionMarker > 0;
myIgnoreBlockTreeSelectionMarker--;
}
}
}
private class MyBlockTreeSelectionListener implements TreeSelectionListener {
private final TextAttributes myAttributes;
public MyBlockTreeSelectionListener() {
myAttributes = new TextAttributes();
myAttributes.setEffectColor(BOX_COLOR);
myAttributes.setEffectType(EffectType.ROUNDED_BOX);
}
@Override
public void valueChanged(@NotNull TreeSelectionEvent e) {
if (myIgnoreBlockTreeSelectionMarker > 0 || myBlockTreeBuilder == null) {
return;
}
Set<?> blockElementsSet = myBlockTreeBuilder.getSelectedElements();
if (blockElementsSet.isEmpty()) return;
BlockTreeNode descriptor = (BlockTreeNode)blockElementsSet.iterator().next();
PsiElement rootPsi = ((ViewerTreeStructure)myPsiTreeBuilder.getTreeStructure()).getRootPsiElement();
int blockStart = descriptor.getBlock().getTextRange().getStartOffset();
PsiFile file = rootPsi.getContainingFile();
PsiElement currentPsiEl = InjectedLanguageUtil.findElementAtNoCommit(file, blockStart);
if (currentPsiEl == null) currentPsiEl = file;
int blockLength = descriptor.getBlock().getTextRange().getLength();
while (currentPsiEl.getParent() != null &&
currentPsiEl.getTextRange().getStartOffset() == blockStart &&
currentPsiEl.getTextLength() != blockLength) {
currentPsiEl = currentPsiEl.getParent();
}
final BlockTreeStructure treeStructure = (BlockTreeStructure)myBlockTreeBuilder.getTreeStructure();
BlockTreeNode rootBlockNode = treeStructure.getRootElement();
int baseOffset = 0;
if (rootBlockNode != null) {
baseOffset = rootBlockNode.getBlock().getTextRange().getStartOffset();
}
if (currentPsiEl != null) {
TextRange range = descriptor.getBlock().getTextRange();
range = range.shiftRight(-baseOffset);
int start = range.getStartOffset();
int end = range.getEndOffset();
final int textLength = myEditor.getDocument().getTextLength();
if (myBlockTree.hasFocus()) {
clearSelection();
if (end <= textLength) {
myHighlighter = myEditor.getMarkupModel()
.addRangeHighlighter(start, end, HighlighterLayer.LAST, myAttributes, HighlighterTargetArea.EXACT_RANGE);
myEditor.getCaretModel().moveToOffset(start);
myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
}
}
updateReferences(currentPsiEl);
if (!myPsiTree.hasFocus()) {
myPsiTreeBuilder.select(currentPsiEl);
}
}
}
}
public void updateReferences(PsiElement element) {
final DefaultListModel model = (DefaultListModel)myRefs.getModel();
model.clear();
final Object cache = myRefs.getClientProperty(REFS_CACHE);
if (cache instanceof Map) {
((Map)cache).clear();
} else {
myRefs.putClientProperty(REFS_CACHE, new HashMap());
}
if (element != null) {
for (PsiReference reference : element.getReferences()) {
model.addElement(reference.getClass().getName());
}
}
}
private void clearSelection() {
if (myHighlighter != null) {
myEditor.getMarkupModel().removeHighlighter(myHighlighter);
myHighlighter.dispose();
}
}
@Override
public void doCancelAction() {
final PsiViewerSettings settings = PsiViewerSettings.getSettings();
final SourceWrapper wrapper = (SourceWrapper)myFileTypeComboBox.getSelectedItem();
if (wrapper != null) settings.type = wrapper.getText();
settings.text = myEditor.getDocument().getText();
settings.showTreeNodes = myShowTreeNodesCheckBox.isSelected();
settings.showWhiteSpaces = myShowWhiteSpacesBox.isSelected();
final Object selectedDialect = myDialectComboBox.getSelectedItem();
settings.dialect = myDialectComboBox.isVisible() && selectedDialect != null ? selectedDialect.toString() : "";
settings.textDividerLocation = myTextSplit.getDividerLocation();
settings.treeDividerLocation = myTreeSplit.getDividerLocation();
settings.showBlocks = myShowBlocksCheckBox.isSelected();
if( myShowBlocksCheckBox.isSelected()) {
settings.blockRefDividerLocation = myBlockRefSplitPane.getDividerLocation();
}
super.doCancelAction();
}
@Override
public void dispose() {
Disposer.dispose(myPsiTreeBuilder);
if (myBlockTreeBuilder != null) {
Disposer.dispose(myBlockTreeBuilder);
}
if (!myEditor.isDisposed()) {
EditorFactory.getInstance().releaseEditor(myEditor);
}
super.dispose();
}
@Nullable
private PsiElement resolve(int index) {
final PsiElement element = getPsiElement();
if (element == null) return null;
@SuppressWarnings("unchecked")
Map<PsiElement, PsiElement[]> map = (Map<PsiElement, PsiElement[]>)myRefs.getClientProperty(REFS_CACHE);
if (map == null) {
myRefs.putClientProperty(REFS_CACHE, map = new HashMap<PsiElement, PsiElement[]>());
}
PsiElement[] cache = map.get(element);
if (cache == null) {
final PsiReference[] references = element.getReferences();
cache = new PsiElement[references.length];
for (int i = 0; i < references.length; i++) {
cache[i] = references[i].resolve();
}
map.put(element, cache);
}
return index >= cache.length ? null : cache[index];
}
@Nullable
private PsiFile getContainingFileForClass(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";
final PsiFile[] files = FilenameIndex.getFilesByName(myProject, filename, GlobalSearchScope.allScope(myProject));
if (files != null && files.length > 0) {
return files[0];
}
return null;
}
@Nullable
public static TreeNode findNodeWithObject(final Object object, final TreeModel model, final Object parent) {
for (int i = 0; i < model.getChildCount(parent); i++) {
final DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) model.getChild(parent, i);
if (childNode.getUserObject().equals(object)) {
return childNode;
} else {
final TreeNode node = findNodeWithObject(object, model, childNode);
if (node != null) return node;
}
}
return null;
}
private class GoToListener implements KeyListener, MouseListener, ListSelectionListener {
private RangeHighlighter myListenerHighlighter;
private final TextAttributes myAttributes =
new TextAttributes(JBColor.RED, null, null, null, Font.PLAIN);
private void navigate() {
final Object value = myRefs.getSelectedValue();
if (value instanceof String) {
final String fqn = (String)value;
final 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();
final int ind = myRefs.getSelectedIndex();
final PsiElement element = getPsiElement();
if (ind > -1 && element != null) {
final PsiReference[] references = element.getReferences();
if (ind < references.length) {
final TextRange textRange = references[ind].getRangeInElement();
TextRange range = InjectedLanguageManager.getInstance(myProject).injectedToHost(element, element.getTextRange());
int start = range.getStartOffset();
int end = range.getEndOffset();
final ViewerTreeStructure treeStructure = (ViewerTreeStructure)myPsiTreeBuilder.getTreeStructure();
PsiElement rootPsiElement = treeStructure.getRootPsiElement();
if (rootPsiElement != null) {
int baseOffset = rootPsiElement.getTextRange().getStartOffset();
start -= baseOffset;
end -= baseOffset;
}
start += textRange.getStartOffset();
end = start + textRange.getLength();
myListenerHighlighter = myEditor.getMarkupModel()
.addRangeHighlighter(start, end, HighlighterLayer.FIRST + 1, myAttributes, 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() {
final Object source = getSource();
final String fileName = "Dummy." + (source instanceof FileType? ((FileType)source).getDefaultExtension() : "txt");
final 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;
}
myEditor.setHighlighter(EditorHighlighterFactory.getInstance().createEditorHighlighter(myProject, lightFile));
}
private class EditorListener extends CaretAdapter implements SelectionListener, DocumentListener {
@Override
public void caretPositionChanged(CaretEvent e) {
if (!available() || myEditor.getSelectionModel().hasSelection()) return;
final ViewerTreeStructure treeStructure = (ViewerTreeStructure)myPsiTreeBuilder.getTreeStructure();
final PsiElement rootPsiElement = treeStructure.getRootPsiElement();
if (rootPsiElement == null) return;
final PsiElement rootElement = ((ViewerTreeStructure)myPsiTreeBuilder.getTreeStructure()).getRootPsiElement();
int baseOffset = rootPsiElement.getTextRange().getStartOffset();
final int offset = myEditor.getCaretModel().getOffset() + baseOffset;
final PsiElement element = InjectedLanguageUtil.findElementAtNoCommit(rootElement.getContainingFile(), offset);
if (element != null && myBlockTreeBuilder != null) {
TextRange rangeInHostFile = InjectedLanguageManager.getInstance(myProject).injectedToHost(element, element.getTextRange());
selectBlockNode(findBlockNode(rangeInHostFile, true));
}
myPsiTreeBuilder.select(element);
}
@Override
public void selectionChanged(SelectionEvent e) {
if (!available() || !myEditor.getSelectionModel().hasSelection()) return;
ViewerTreeStructure treeStructure = (ViewerTreeStructure)myPsiTreeBuilder.getTreeStructure();
if (treeStructure == null) return;
final PsiElement rootElement = treeStructure.getRootPsiElement();
if (rootElement == null) return;
final SelectionModel selection = myEditor.getSelectionModel();
final TextRange textRange = rootElement.getTextRange();
int baseOffset = textRange != null ? textRange.getStartOffset() : 0;
final int start = selection.getSelectionStart()+baseOffset;
final int end = selection.getSelectionEnd()+baseOffset - 1;
final PsiElement element =
findCommonParent(InjectedLanguageUtil.findElementAtNoCommit(rootElement.getContainingFile(), start),
InjectedLanguageUtil.findElementAtNoCommit(rootElement.getContainingFile(), end));
if (element != null && myBlockTreeBuilder != null) {
if (myEditor.getContentComponent().hasFocus()) {
TextRange rangeInHostFile = InjectedLanguageManager.getInstance(myProject).injectedToHost(element, element.getTextRange());
selectBlockNode(findBlockNode(rangeInHostFile, true));
}
}
myPsiTreeBuilder.select(element);
}
@Nullable
private PsiElement findCommonParent(PsiElement start, PsiElement end) {
if (end == null || start == end) {
return start;
}
final 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 beforeDocumentChange(DocumentEvent event) {
}
@Override
public void documentChanged(DocumentEvent event) {
myNewDocumentHashCode = event.getDocument().getText().hashCode();
}
}
private static class AutoExpandFocusListener extends FocusAdapter {
private final JComboBox myComboBox;
private final Component myParent;
private AutoExpandFocusListener(final JComboBox comboBox) {
myComboBox = comboBox;
myParent = UIUtil.findUltimateParent(myComboBox);
}
@Override
public void focusGained(@NotNull final FocusEvent e) {
final Component from = e.getOppositeComponent();
if (!e.isTemporary() && from != null && !myComboBox.isPopupVisible() && isUnder(from, myParent)) {
myComboBox.setPopupVisible(true);
}
}
private static boolean isUnder(Component component, final Component parent) {
while (component != null) {
if (component == parent) return true;
component = component.getParent();
}
return false;
}
}
@Nullable
private BlockTreeNode findBlockNode(TextRange range, boolean selectParentIfNotFound) {
final BlockTreeBuilder builder = myBlockTreeBuilder;
if (builder == null || !myBlockStructurePanel.isVisible()) {
return null;
}
AbstractTreeStructure treeStructure = builder.getTreeStructure();
if (treeStructure == null) return null;
BlockTreeNode node = (BlockTreeNode)treeStructure.getRootElement();
main_loop:
while (true) {
if (node.getBlock().getTextRange().equals(range)) {
return node;
}
for (BlockTreeNode child : node.getChildren()) {
if (child.getBlock().getTextRange().contains(range)) {
node = child;
continue main_loop;
}
}
return selectParentIfNotFound ? node : null;
}
}
}