processing.mode.java.pdex.PDEX Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of java-mode Show documentation
Show all versions of java-mode Show documentation
Processing is a programming language, development environment, and online community.
This Java Mode package contains the Java mode for Processing IDE.
package processing.mode.java.pdex;
import com.google.classpath.ClassPath;
import com.google.classpath.ClassPathFactory;
import com.google.classpath.RegExpResourceFilter;
import org.eclipse.jdt.core.compiler.IProblem;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclaration;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JTree;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeModel;
import processing.app.Language;
import processing.app.Messages;
import processing.app.Platform;
import processing.app.Problem;
import processing.app.Sketch;
import processing.app.SketchCode;
import processing.app.syntax.SyntaxDocument;
import processing.app.ui.EditorStatus;
import processing.app.ui.Toolkit;
import processing.app.ui.ZoomTreeCellRenderer;
import processing.mode.java.JavaEditor;
import processing.mode.java.JavaMode;
import processing.mode.java.pdex.PreprocessedSketch.SketchInterval;
import static processing.mode.java.pdex.ASTUtils.*;
public class PDEX {
private static final boolean SHOW_DEBUG_TREE = false;
private boolean enabled = true;
private ErrorChecker errorChecker;
private InspectMode inspectMode;
private ShowUsage showUsage;
private Rename rename;
private DebugTree debugTree;
private PreprocessingService pps;
public PDEX(JavaEditor editor, PreprocessingService pps) {
this.pps = pps;
this.enabled = !editor.hasJavaTabs();
errorChecker = new ErrorChecker(editor, pps);
inspectMode = new InspectMode(editor, pps);
showUsage = new ShowUsage(editor, pps);
rename = new Rename(editor, pps);
if (SHOW_DEBUG_TREE) {
debugTree = new DebugTree(editor, pps);
}
for (SketchCode code : editor.getSketch().getCode()) {
Document document = code.getDocument();
addDocumentListener(document);
}
sketchChanged();
}
public void addDocumentListener(Document doc) {
if (doc != null) doc.addDocumentListener(sketchChangedListener);
}
protected final DocumentListener sketchChangedListener = new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
sketchChanged();
}
@Override
public void removeUpdate(DocumentEvent e) {
sketchChanged();
}
@Override
public void changedUpdate(DocumentEvent e) {
sketchChanged();
}
};
public void sketchChanged() {
errorChecker.notifySketchChanged();
pps.notifySketchChanged();
}
public void preferencesChanged() {
errorChecker.preferencesChanged();
sketchChanged();
}
public void hasJavaTabsChanged(boolean hasJavaTabs) {
enabled = !hasJavaTabs;
if (!enabled) {
showUsage.hide();
}
}
public void dispose() {
inspectMode.dispose();
errorChecker.dispose();
showUsage.dispose();
rename.dispose();
if (debugTree != null) {
debugTree.dispose();
}
}
public void documentChanged(Document newDoc) {
addDocumentListener(newDoc);
}
private class InspectMode {
boolean inspectModeEnabled;
boolean isMouse1Down;
boolean isMouse2Down;
boolean isHotkeyDown;
Predicate mouseEventHotkeyTest = Platform.isMacOS() ?
InputEvent::isMetaDown : InputEvent::isControlDown;
Predicate keyEventHotkeyTest = Platform.isMacOS() ?
e -> e.getKeyCode() == KeyEvent.VK_META :
e -> e.getKeyCode() == KeyEvent.VK_CONTROL;
JavaEditor editor;
PreprocessingService pps;
InspectMode(JavaEditor editor, PreprocessingService pps) {
this.editor = editor;
this.pps = pps;
// Add listeners
JMenuItem showUsageItem = new JMenuItem(Language.text("editor.popup.jump_to_declaration"));
showUsageItem.addActionListener(e -> handleInspect());
editor.getTextArea().getRightClickPopup().add(showUsageItem);
editor.getJavaTextArea().getPainter().addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
isMouse1Down = isMouse1Down || (e.getButton() == MouseEvent.BUTTON1);
isMouse2Down = isMouse2Down || (e.getButton() == MouseEvent.BUTTON2);
}
@Override
public void mouseReleased(MouseEvent e) {
boolean releasingMouse1 = e.getButton() == MouseEvent.BUTTON1;
boolean releasingMouse2 = e.getButton() == MouseEvent.BUTTON2;
if (JavaMode.inspectModeHotkeyEnabled && inspectModeEnabled &&
isMouse1Down && releasingMouse1) {
handleInspect(e);
} else if (!inspectModeEnabled && isMouse2Down && releasingMouse2) {
handleInspect(e);
}
isMouse1Down = isMouse1Down && !releasingMouse1;
isMouse2Down = isMouse2Down && !releasingMouse2;
}
});
editor.getJavaTextArea().getPainter().addMouseMotionListener(new MouseAdapter() {
@Override
public void mouseDragged(MouseEvent e) {
if (editor.isSelectionActive()) {
// Mouse was dragged too much, disable
inspectModeEnabled = false;
// Cancel possible mouse 2 press
isMouse2Down = false;
}
}
@Override
public void mouseMoved(MouseEvent e) {
isMouse1Down = false;
isMouse2Down = false;
isHotkeyDown = mouseEventHotkeyTest.test(e);
inspectModeEnabled = isHotkeyDown;
}
});
editor.getJavaTextArea().addMouseWheelListener(new MouseAdapter() {
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
// Editor was scrolled while mouse 1 was pressed, disable
if (isMouse1Down) inspectModeEnabled = false;
}
});
editor.getJavaTextArea().addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
isHotkeyDown = isHotkeyDown || keyEventHotkeyTest.test(e);
// Enable if hotkey was just pressed and mouse 1 is not down
inspectModeEnabled = inspectModeEnabled || (!isMouse1Down && isHotkeyDown);
}
@Override
public void keyReleased(KeyEvent e) {
isHotkeyDown = isHotkeyDown && !keyEventHotkeyTest.test(e);
// Disable if hotkey was just released
inspectModeEnabled = inspectModeEnabled && isHotkeyDown;
}
});
}
void handleInspect() {
int off = editor.getSelectionStart();
int tabIndex = editor.getSketch().getCurrentCodeIndex();
pps.whenDoneBlocking(ps -> handleInspect(ps, tabIndex, off));
}
// Thread: EDT
void handleInspect(MouseEvent evt) {
int off = editor.getJavaTextArea().xyToOffset(evt.getX(), evt.getY());
if (off < 0) return;
int tabIndex = editor.getSketch().getCurrentCodeIndex();
pps.whenDoneBlocking(ps -> handleInspect(ps, tabIndex, off));
}
// Thread: worker
private void handleInspect(PreprocessedSketch ps, int tabIndex, int offset) {
ASTNode root = ps.compilationUnit;
int javaOffset = ps.tabOffsetToJavaOffset(tabIndex, offset);
SimpleName simpleName = getSimpleNameAt(root, javaOffset, javaOffset);
if (simpleName == null) {
Messages.log("no simple name found at click location");
return;
}
IBinding binding = resolveBinding(simpleName);
if (binding == null) {
Messages.log("binding not resolved");
return;
}
String key = binding.getKey();
ASTNode decl = ps.compilationUnit.findDeclaringNode(key);
if (decl == null) {
Messages.log("decl not found, showing usage instead");
showUsage.findUsageAndUpdateTree(ps, binding);
return;
}
SimpleName declName = null;
switch (binding.getKind()) {
case IBinding.TYPE: declName = ((TypeDeclaration) decl).getName(); break;
case IBinding.METHOD: declName = ((MethodDeclaration) decl).getName(); break;
case IBinding.VARIABLE: declName = ((VariableDeclaration) decl).getName(); break;
}
if (declName == null) {
Messages.log("decl name not found " + decl);
return;
}
if (declName.equals(simpleName)) {
showUsage.findUsageAndUpdateTree(ps, binding);
} else {
Messages.log("found declaration, offset " + decl.getStartPosition() + ", name: " + declName);
SketchInterval si = ps.mapJavaToSketch(declName);
if (!ps.inRange(si)) return;
EventQueue.invokeLater(() -> {
editor.highlight(si.tabIndex, si.startTabOffset, si.stopTabOffset);
});
}
}
void dispose() {
// Nothing to do
}
}
static private class ShowUsage {
final JDialog window;
final JTree tree;
final JavaEditor editor;
final PreprocessingService pps;
final Consumer reloadListener;
IBinding binding;
ShowUsage(JavaEditor editor, PreprocessingService pps) {
this.editor = editor;
this.pps = pps;
// Add show usage option
JMenuItem showUsageItem =
new JMenuItem(Language.text("editor.popup.show_usage"));
showUsageItem.addActionListener(e -> handleShowUsage());
editor.getTextArea().getRightClickPopup().add(showUsageItem);
reloadListener = this::reloadShowUsage;
{ // Show Usage window
window = new JDialog(editor);
window.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
window.setAutoRequestFocus(false);
window.addComponentListener(new ComponentAdapter() {
@Override
public void componentHidden(ComponentEvent e) {
binding = null;
tree.setModel(null);
pps.unregisterListener(reloadListener);
}
@Override
public void componentShown(ComponentEvent e) {
pps.registerListener(reloadListener);
}
});
window.setSize(Toolkit.zoom(300, 400));
window.setFocusableWindowState(false);
Toolkit.setIcon(window);
JScrollPane sp2 = new JScrollPane();
tree = new JTree();
ZoomTreeCellRenderer renderer =
new ZoomTreeCellRenderer(editor.getMode());
tree.setCellRenderer(renderer);
renderer.setLeafIcon(null);
renderer.setClosedIcon(null);
renderer.setOpenIcon(null);
renderer.setBackgroundSelectionColor(new Color(228, 248, 246));
renderer.setBorderSelectionColor(new Color(0, 0, 0, 0));
renderer.setTextSelectionColor(Color.BLACK);
sp2.setViewportView(tree);
window.add(sp2);
}
tree.addTreeSelectionListener(e -> {
if (tree.getLastSelectedPathComponent() != null) {
DefaultMutableTreeNode tnode =
(DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
if (tnode.getUserObject() instanceof ShowUsageTreeNode) {
ShowUsageTreeNode node = (ShowUsageTreeNode) tnode.getUserObject();
editor.highlight(node.tabIndex, node.startTabOffset, node.stopTabOffset);
}
}
});
}
// Thread: EDT
void handleShowUsage() {
int startOffset = editor.getSelectionStart();
int stopOffset = editor.getSelectionStop();
int tabIndex = editor.getSketch().getCurrentCodeIndex();
pps.whenDoneBlocking(ps -> handleShowUsage(ps, tabIndex, startOffset, stopOffset));
}
// Thread: worker
void handleShowUsage(PreprocessedSketch ps, int tabIndex,
int startTabOffset, int stopTabOffset) {
// Map offsets
int startJavaOffset = ps.tabOffsetToJavaOffset(tabIndex, startTabOffset);
int stopJavaOffset = ps.tabOffsetToJavaOffset(tabIndex, stopTabOffset);
// Find the node
SimpleName name = ASTUtils.getSimpleNameAt(ps.compilationUnit, startJavaOffset, stopJavaOffset);
if (name == null) {
editor.statusMessage("Cannot find any name under cursor", EditorStatus.NOTICE);
return;
}
// Find binding
IBinding binding = ASTUtils.resolveBinding(name);
if (binding == null) {
editor.statusMessage("Cannot find usages, try to fix errors in your code first",
EditorStatus.NOTICE);
return;
}
findUsageAndUpdateTree(ps, binding);
}
// Thread: worker
void findUsageAndUpdateTree(PreprocessedSketch ps, IBinding binding) {
this.binding = binding;
// Get label
String bindingType = "";
switch (binding.getKind()) {
case IBinding.METHOD:
IMethodBinding method = (IMethodBinding) binding;
if (method.isConstructor()) bindingType = "Constructor";
else bindingType = "Method";
break;
case IBinding.TYPE:
bindingType = "Type";
break;
case IBinding.VARIABLE:
IVariableBinding variable = (IVariableBinding) binding;
if (variable.isField()) bindingType = "Field";
else if (variable.isParameter()) bindingType = "Parameter";
else if (variable.isEnumConstant()) bindingType = "Enum constant";
else bindingType = "Local variable";
break;
}
// Find usages, map to tree nodes, add to root node
String bindingKey = binding.getKey();
List intervals =
findAllOccurrences(ps.compilationUnit, bindingKey).stream()
.map(ps::mapJavaToSketch)
// remove occurrences which fall into generated header
.filter(ps::inRange)
// remove empty intervals (happens when occurence was inserted)
.filter(in -> in.startPdeOffset < in.stopPdeOffset)
.collect(Collectors.toList());
int usageCount = intervals.size();
// Get element name from PDE code if possible, otherwise use one from Java
String elementName = intervals.stream()
.findAny()
.map(si -> ps.pdeCode.substring(si.startPdeOffset, si.stopPdeOffset))
.orElseGet(binding::getName);
// Create root node
DefaultMutableTreeNode rootNode =
new DefaultMutableTreeNode(bindingType + ": " + elementName);
intervals.stream()
// Convert to TreeNodes
.map(in -> ShowUsageTreeNode.fromSketchInterval(ps, in))
// Group by tab index
.collect(Collectors.groupingBy(node -> node.tabIndex))
// Stream Map Entries of (tab index) <-> (List)
.entrySet().stream()
// Sort by tab index
.sorted(Comparator.comparing(Map.Entry::getKey))
.map(entry -> {
Integer tabIndex = entry.getKey();
List nodes = entry.getValue();
int count = nodes.size();
String usageLabel = count == 1 ? "usage" : "usages";
// Create new DefaultMutableTreeNode for this tab
String tabLabel = "" +
ps.sketch.getCode(tabIndex).getPrettyName() +
" " + count + " " + usageLabel + "";
DefaultMutableTreeNode tabNode = new DefaultMutableTreeNode(tabLabel);
// Stream nodes belonging to this tab
nodes.stream()
// Convert TreeNodes to DefaultMutableTreeNodes
.map(DefaultMutableTreeNode::new)
// Add all as children of tab node
.forEach(tabNode::add);
return tabNode;
})
// Add all tab nodes as children of root node
.forEach(rootNode::add);
TreeModel treeModel = new DefaultTreeModel(rootNode);
// Update tree
EventQueue.invokeLater(() -> {
tree.setModel(treeModel);
// Expand all nodes
for (int i = 0; i < tree.getRowCount(); i++) {
tree.expandRow(i);
}
tree.setRootVisible(true);
if (!window.isVisible()) {
window.setVisible(true);
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice defaultScreen = ge.getDefaultScreenDevice();
Rectangle rect = defaultScreen.getDefaultConfiguration().getBounds();
int maxX = (int) rect.getMaxX() - window.getWidth();
int x = Math.min(editor.getX() + editor.getWidth(), maxX);
int y = (x == maxX) ? 10 : editor.getY();
window.setLocation(x, y);
}
window.toFront();
window.setTitle("Usage of \"" + elementName + "\" : " +
usageCount + " time(s)");
});
}
// Thread: worker
void reloadShowUsage(PreprocessedSketch ps) {
if (binding != null) {
findUsageAndUpdateTree(ps, binding);
}
}
void hide() {
window.setVisible(false);
}
void dispose() {
if (window != null) {
window.dispose();
}
}
}
static private class ShowUsageTreeNode {
final int tabIndex;
final int startTabOffset;
final int stopTabOffset;
final String text;
ShowUsageTreeNode(int tabIndex, int startTabOffset, int stopTabOffset, String text) {
this.tabIndex = tabIndex;
this.startTabOffset = startTabOffset;
this.stopTabOffset = stopTabOffset;
this.text = text;
}
static ShowUsageTreeNode fromSketchInterval(PreprocessedSketch ps, SketchInterval in) {
int lineStartPdeOffset = ps.pdeCode.lastIndexOf('\n', in.startPdeOffset) + 1;
int lineStopPdeOffset = ps.pdeCode.indexOf('\n', in.stopPdeOffset);
if (lineStopPdeOffset == -1) lineStopPdeOffset = ps.pdeCode.length();
int highlightStartOffset = in.startPdeOffset - lineStartPdeOffset;
int highlightStopOffset = in.stopPdeOffset - lineStartPdeOffset;
int tabLine = ps.tabOffsetToTabLine(in.tabIndex, in.startTabOffset);
// TODO: what a mess
String line = ps.pdeCode.substring(lineStartPdeOffset, lineStopPdeOffset);
String pre = line.substring(0, highlightStartOffset)
.replace("&", "&").replace(">", ">").replace("<", "<");
String highlight = line.substring(highlightStartOffset, highlightStopOffset)
.replace("&", "&").replace(">", ">").replace("<", "<");
String post = line.substring(highlightStopOffset)
.replace("&", "&").replace(">", ">").replace("<", "<");
line = pre + "" + highlight + "" + post;
line = line.trim();
String text = "" +
(tabLine + 1) + " " + line + "";
return new ShowUsageTreeNode(in.tabIndex, in.startTabOffset, in.stopTabOffset, text);
}
@Override
public String toString() {
return text;
}
}
private class Rename {
final JDialog window;
final JTextField textField;
final JLabel oldNameLabel;
final JavaEditor editor;
final PreprocessingService pps;
IBinding binding;
PreprocessedSketch ps;
Rename(JavaEditor editor, PreprocessingService pps) {
this.editor = editor;
this.pps = pps;
// Add rename option
JMenuItem renameItem = new JMenuItem(Language.text("editor.popup.rename"));
renameItem.addActionListener(e -> handleRename());
editor.getTextArea().getRightClickPopup().add(renameItem);
window = new JDialog(editor);
window.setTitle("Enter new name:");
window.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
window.setModal(true);
window.setResizable(false);
window.addComponentListener(new ComponentAdapter() {
@Override
public void componentHidden(ComponentEvent e) {
binding = null;
ps = null;
}
});
window.setSize(250, 130);
window.setLayout(new BoxLayout(window.getContentPane(), BoxLayout.Y_AXIS));
Toolkit.setIcon(window);
{ // Top panel
// Text field
textField = new JTextField();
textField.setPreferredSize(new Dimension(150, 60));
// Old name label
oldNameLabel = new JLabel();
oldNameLabel.setText("Old Name: ");
// Top panel
JPanel panelTop = new JPanel();
panelTop.setLayout(new BoxLayout(panelTop, BoxLayout.Y_AXIS));
panelTop.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
panelTop.add(textField);
panelTop.add(Box.createRigidArea(new Dimension(0, 10)));
panelTop.add(oldNameLabel);
window.add(panelTop);
}
{ // Bottom panel
JButton showUsageButton = new JButton("Show Usage");
showUsageButton.addActionListener(e -> {
showUsage.findUsageAndUpdateTree(ps, binding);
window.setVisible(false);
});
JButton renameButton = new JButton("Rename");
renameButton.addActionListener(e -> {
if (textField.getText().length() == 0) {
return;
}
String newName = textField.getText().trim();
boolean isNewNameValid = newName.length() >= 1 &&
newName.chars().limit(1).allMatch(Character::isUnicodeIdentifierStart) &&
newName.chars().skip(1).allMatch(Character::isUnicodeIdentifierPart);
if (!isNewNameValid) {
JOptionPane.showMessageDialog(new JFrame(), "'" + newName
+ "' isn't a valid name.", "Uh oh..", JOptionPane.PLAIN_MESSAGE);
} else {
rename(ps, binding, newName);
window.setVisible(false);
}
});
JPanel panelBottom = new JPanel();
panelBottom.setLayout(new BoxLayout(panelBottom, BoxLayout.X_AXIS));
panelBottom.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
panelBottom.add(Box.createHorizontalGlue());
panelBottom.add(showUsageButton);
panelBottom.add(Box.createRigidArea(new Dimension(15, 0)));
panelBottom.add(renameButton);
window.add(panelBottom);
}
window.setMinimumSize(window.getSize());
}
// Thread: EDT
void handleRename() {
int startOffset = editor.getSelectionStart();
int stopOffset = editor.getSelectionStop();
int tabIndex = editor.getSketch().getCurrentCodeIndex();
pps.whenDoneBlocking(ps -> handleRename(ps, tabIndex, startOffset, stopOffset));
}
// Thread: worker
void handleRename(PreprocessedSketch ps, int tabIndex, int startTabOffset, int stopTabOffset) {
if (ps.hasSyntaxErrors) {
editor.statusMessage("Can't perform action until syntax errors are fixed",
EditorStatus.WARNING);
return;
}
ASTNode root = ps.compilationUnit;
// Map offsets
int startJavaOffset = ps.tabOffsetToJavaOffset(tabIndex, startTabOffset);
int stopJavaOffset = ps.tabOffsetToJavaOffset(tabIndex, stopTabOffset);
// Find the node
SimpleName name = getSimpleNameAt(root, startJavaOffset, stopJavaOffset);
if (name == null) {
editor.statusMessage("Highlight the class/function/variable name first",
EditorStatus.NOTICE);
return;
}
// Find binding
IBinding binding = resolveBinding(name);
if (binding == null) {
editor.statusMessage(name.getIdentifier() + " isn't defined in this sketch, " +
"so it cannot be renamed", EditorStatus.ERROR);
return;
}
ASTNode decl = ps.compilationUnit.findDeclaringNode(binding.getKey());
if (decl == null) {
editor.statusMessage(name.getIdentifier() + " isn't defined in this sketch, " +
"so it cannot be renamed", EditorStatus.ERROR);
return;
}
// Display the rename dialog
EventQueue.invokeLater(() -> {
if (!window.isVisible()) {
this.ps = ps;
this.binding = binding;
oldNameLabel.setText("Current name: " + binding.getName());
textField.setText(binding.getName());
textField.requestFocus();
textField.selectAll();
int x = editor.getX() + (editor.getWidth() - window.getWidth()) / 2;
int y = editor.getY() + (editor.getHeight() - window.getHeight()) / 2;
window.setLocation(x, y);
window.setVisible(true);
window.toFront();
}
});
}
// Thread: EDT (we can't allow user to mess with sketch while renaming)
void rename(PreprocessedSketch ps, IBinding binding, String newName) {
CompilationUnit root = ps.compilationUnit;
// Renaming constructor should rename class
if (binding.getKind() == IBinding.METHOD) {
IMethodBinding method = (IMethodBinding) binding;
if (method.isConstructor()) {
binding = method.getDeclaringClass();
}
}
ASTNode decl = root.findDeclaringNode(binding.getKey());
if (decl == null) return;
showUsage.hide();
List occurrences = new ArrayList<>();
occurrences.addAll(findAllOccurrences(root, binding.getKey()));
// Renaming class should rename all constructors
if (binding.getKind() == IBinding.TYPE) {
ITypeBinding type = (ITypeBinding) binding;
//type = type.getErasure();
IMethodBinding[] methods = type.getDeclaredMethods();
Arrays.stream(methods)
.filter(IMethodBinding::isConstructor)
.flatMap(c -> findAllOccurrences(root, c.getKey()).stream())
.forEach(occurrences::add);
}
Map> mappedNodes = occurrences.stream()
.map(ps::mapJavaToSketch)
.filter(ps::inRange)
.collect(Collectors.groupingBy(interval -> interval.tabIndex));
Sketch sketch = ps.sketch;
editor.startCompoundEdit();
mappedNodes.entrySet().forEach(entry -> {
int tabIndex = entry.getKey();
SketchCode sketchCode = sketch.getCode(tabIndex);
SyntaxDocument document = (SyntaxDocument) sketchCode.getDocument();
List nodes = entry.getValue();
nodes.stream()
// Replace from the end so all unprocess offsets stay valid
.sorted(Comparator.comparing((SketchInterval si) -> si.startTabOffset).reversed())
.forEach(si -> {
// Make sure offsets are in bounds
int documentLength = document.getLength();
if (si.startTabOffset >= 0 && si.startTabOffset <= documentLength &&
si.stopTabOffset >= 0 && si.stopTabOffset <= documentLength) {
// Replace the code
int length = si.stopTabOffset - si.startTabOffset;
try {
document.remove(si.startTabOffset, length);
document.insertString(si.startTabOffset, newName, null);
} catch (BadLocationException e) { /* Whatever */ }
}
});
try {
sketchCode.setProgram(document.getText(0, document.getLength()));
} catch (BadLocationException e) { /* Whatever */ }
sketchCode.setModified(true);
});
editor.stopCompoundEdit();
editor.repaintHeader();
int currentTabIndex = sketch.getCurrentCodeIndex();
final int currentOffset = editor.getCaretOffset();
int precedingIntervals =
(int) mappedNodes.getOrDefault(currentTabIndex, Collections.emptyList())
.stream()
.filter(interval -> interval.stopTabOffset < currentOffset)
.count();
int intervalLengthDiff = newName.length() - binding.getName().length();
int offsetDiff = precedingIntervals * intervalLengthDiff;
editor.getTextArea().setCaretPosition(currentOffset + offsetDiff);
}
void dispose() {
if (window != null) {
window.dispose();
}
}
}
static private class DebugTree {
final JDialog window;
final JTree tree;
final Consumer updateListener;
DebugTree(JavaEditor editor, PreprocessingService pps) {
updateListener = this::buildAndUpdateTree;
window = new JDialog(editor);
tree = new JTree() {
@Override
public String convertValueToText(Object value, boolean selected,
boolean expanded, boolean leaf,
int row, boolean hasFocus) {
if (value instanceof DefaultMutableTreeNode) {
DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode) value;
Object o = treeNode.getUserObject();
if (o instanceof ASTNode) {
ASTNode node = (ASTNode) o;
return CompletionGenerator.getNodeAsString(node);
}
}
return super.convertValueToText(value, selected, expanded, leaf, row, hasFocus);
}
};
tree.setCellRenderer(new ZoomTreeCellRenderer(editor.getMode()));
window.addComponentListener(new ComponentAdapter() {
@Override
public void componentHidden(ComponentEvent e) {
pps.unregisterListener(updateListener);
tree.setModel(null);
}
});
window.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
window.setBounds(new Rectangle(680, 100, 460, 620));
window.setTitle("AST View - " + editor.getSketch().getName());
JScrollPane sp = new JScrollPane();
sp.setViewportView(tree);
window.add(sp);
pps.whenDone(updateListener);
pps.registerListener(updateListener);
tree.addTreeSelectionListener(e -> {
if (tree.getLastSelectedPathComponent() != null) {
DefaultMutableTreeNode tnode =
(DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
if (tnode.getUserObject() instanceof ASTNode) {
ASTNode node = (ASTNode) tnode.getUserObject();
pps.whenDone(ps -> {
SketchInterval si = ps.mapJavaToSketch(node);
if (!ps.inRange(si)) return;
EventQueue.invokeLater(() -> {
editor.highlight(si.tabIndex, si.startTabOffset, si.stopTabOffset);
});
});
}
}
});
}
void dispose() {
if (window != null) {
window.dispose();
}
}
// Thread: worker
void buildAndUpdateTree(PreprocessedSketch ps) {
CompilationUnit cu = ps.compilationUnit;
if (cu.types().isEmpty()){
Messages.loge("No Type found in CU");
return;
}
Deque treeNodeStack = new ArrayDeque<>();
ASTNode type0 = (ASTNode) cu.types().get(0);
type0.accept(new ASTVisitor() {
@Override
public boolean preVisit2(ASTNode node) {
treeNodeStack.push(new DefaultMutableTreeNode(node));
return super.preVisit2(node);
}
@Override
public void postVisit(ASTNode node) {
if (treeNodeStack.size() > 1) {
DefaultMutableTreeNode treeNode = treeNodeStack.pop();
treeNodeStack.peek().add(treeNode);
}
}
});
DefaultMutableTreeNode codeTree = treeNodeStack.pop();
EventQueue.invokeLater(() -> {
if (tree.hasFocus() || window.hasFocus()) {
return;
}
tree.setModel(new DefaultTreeModel(codeTree));
((DefaultTreeModel) tree.getModel()).reload();
tree.validate();
if (!window.isVisible()) {
window.setVisible(true);
}
});
}
}
static private class ErrorChecker {
// Delay delivering error check result after last sketch change #2677
private final static long DELAY_BEFORE_UPDATE = 650;
private ScheduledExecutorService scheduler;
private volatile ScheduledFuture> scheduledUiUpdate = null;
private volatile long nextUiUpdate = 0;
private volatile boolean enabled = true;
private final Consumer errorHandlerListener = this::handleSketchProblems;
private JavaEditor editor;
private PreprocessingService pps;
public ErrorChecker(JavaEditor editor, PreprocessingService pps) {
this.editor = editor;
this.pps = pps;
scheduler = Executors.newSingleThreadScheduledExecutor();
this.enabled = JavaMode.errorCheckEnabled;
if (enabled) {
pps.registerListener(errorHandlerListener);
}
}
public void notifySketchChanged() {
nextUiUpdate = System.currentTimeMillis() + DELAY_BEFORE_UPDATE;
}
public void preferencesChanged() {
if (enabled != JavaMode.errorCheckEnabled) {
enabled = JavaMode.errorCheckEnabled;
if (enabled) {
pps.registerListener(errorHandlerListener);
} else {
pps.unregisterListener(errorHandlerListener);
editor.setProblemList(Collections.emptyList());
nextUiUpdate = 0;
}
}
}
public void dispose() {
if (scheduler != null) {
scheduler.shutdownNow();
}
}
private void handleSketchProblems(PreprocessedSketch ps) {
Map suggCache =
JavaMode.importSuggestEnabled ? new HashMap<>() : Collections.emptyMap();
final List problems = new ArrayList<>();
IProblem[] iproblems = ps.compilationUnit.getProblems();
{ // Handle missing brace problems
IProblem missingBraceProblem = Arrays.stream(iproblems)
.filter(ErrorChecker::isMissingBraceProblem)
.findFirst()
// Ignore if it is at the end of file
.filter(p -> p.getSourceEnd() + 1 < ps.javaCode.length())
// Ignore if the tab number does not match our detected tab number
.filter(p -> ps.missingBraceProblems.isEmpty() ||
ps.missingBraceProblems.get(0).getTabIndex() ==
ps.mapJavaToSketch(p.getSourceStart(), p.getSourceEnd()+1).tabIndex
)
.orElse(null);
// If there is missing brace ignore all other problems
if (missingBraceProblem != null) {
// Prefer ECJ problem, shows location more accurately
iproblems = new IProblem[]{missingBraceProblem};
} else if (!ps.missingBraceProblems.isEmpty()) {
// Fallback to manual detection
problems.addAll(ps.missingBraceProblems);
}
}
AtomicReference searchClassPath = new AtomicReference<>(null);
if (problems.isEmpty()) {
List cuProblems = Arrays.stream(iproblems)
// Filter Warnings if they are not enabled
.filter(iproblem -> !(iproblem.isWarning() && !JavaMode.warningsEnabled))
// Hide a useless error which is produced when a line ends with
// an identifier without a semicolon. "Missing a semicolon" is
// also produced and is preferred over this one.
// (Syntax error, insert ":: IdentifierOrNew" to complete Expression)
// See: https://bugs.eclipse.org/bugs/show_bug.cgi?id=405780
.filter(iproblem -> !iproblem.getMessage()
.contains("Syntax error, insert \":: IdentifierOrNew\""))
// Transform into our Problems
.map(iproblem -> {
int start = iproblem.getSourceStart();
int stop = iproblem.getSourceEnd() + 1; // make it exclusive
SketchInterval in = ps.mapJavaToSketch(start, stop);
if (in == SketchInterval.BEFORE_START) return null;
int line = ps.tabOffsetToTabLine(in.tabIndex, in.startTabOffset);
JavaProblem p = JavaProblem.fromIProblem(iproblem, in.tabIndex, line);
p.setPDEOffsets(in.startTabOffset, in.stopTabOffset);
// Handle import suggestions
if (JavaMode.importSuggestEnabled && isUndefinedTypeProblem(iproblem)) {
ClassPath cp = searchClassPath.updateAndGet(prev -> prev != null ?
prev : new ClassPathFactory().createFromPaths(ps.searchClassPathArray));
String[] s = suggCache.computeIfAbsent(iproblem.getArguments()[0],
name -> getImportSuggestions(cp, name));
p.setImportSuggestions(s);
}
return p;
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
problems.addAll(cuProblems);
}
if (scheduledUiUpdate != null) {
scheduledUiUpdate.cancel(true);
}
// Update UI after a delay. See #2677
long delay = nextUiUpdate - System.currentTimeMillis();
Runnable uiUpdater = () -> {
if (nextUiUpdate > 0 && System.currentTimeMillis() >= nextUiUpdate) {
EventQueue.invokeLater(() -> editor.setProblemList(problems));
}
};
scheduledUiUpdate = scheduler.schedule(uiUpdater, delay,
TimeUnit.MILLISECONDS);
}
static private boolean isUndefinedTypeProblem(IProblem iproblem) {
int id = iproblem.getID();
return id == IProblem.UndefinedType ||
id == IProblem.UndefinedName ||
id == IProblem.UnresolvedVariable;
}
static private boolean isMissingBraceProblem(IProblem iproblem) {
switch (iproblem.getID()) {
case IProblem.ParsingErrorInsertToComplete: {
char brace = iproblem.getArguments()[0].charAt(0);
return brace == '{' || brace == '}';
}
case IProblem.ParsingErrorInsertTokenAfter: {
char brace = iproblem.getArguments()[1].charAt(0);
return brace == '{' || brace == '}';
}
default:
return false;
}
}
static public String[] getImportSuggestions(ClassPath cp, String className) {
RegExpResourceFilter regf = new RegExpResourceFilter(
Pattern.compile(".*"),
Pattern.compile("(.*\\$)?" + className + "\\.class",
Pattern.CASE_INSENSITIVE));
String[] resources = cp.findResources("", regf);
return Arrays.stream(resources)
// remove ".class" suffix
.map(res -> res.substring(0, res.length() - 6))
// replace path separators with dots
.map(res -> res.replace('/', '.'))
// replace inner class separators with dots
.map(res -> res.replace('$', '.'))
// sort, prioritize clases from java. package
.sorted((o1, o2) -> {
// put java.* first, should be prioritized more
boolean o1StartsWithJava = o1.startsWith("java");
boolean o2StartsWithJava = o2.startsWith("java");
if (o1StartsWithJava != o2StartsWithJava) {
if (o1StartsWithJava) return -1;
return 1;
}
return o1.compareTo(o2);
})
.toArray(String[]::new);
}
}
}