mirror of
https://github.com/processing/processing4.git
synced 2026-01-30 11:51:54 +01:00
1321 lines
44 KiB
Java
1321 lines
44 KiB
Java
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.Matcher;
|
|
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<MouseEvent> mouseEventHotkeyTest = Platform.isMacOS() ?
|
|
InputEvent::isMetaDown : InputEvent::isControlDown;
|
|
Predicate<KeyEvent> 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<PreprocessedSketch> 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<SketchInterval> 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<ShowUsageTreeNode>)
|
|
.entrySet().stream()
|
|
// Sort by tab index
|
|
.sorted(Comparator.comparing(Map.Entry::getKey))
|
|
.map(entry -> {
|
|
Integer tabIndex = entry.getKey();
|
|
List<ShowUsageTreeNode> nodes = entry.getValue();
|
|
|
|
int count = nodes.size();
|
|
String usageLabel = count == 1 ? "usage" : "usages";
|
|
|
|
// Create new DefaultMutableTreeNode for this tab
|
|
String tabLabel = "<html><font color=#222222>" +
|
|
ps.sketch.getCode(tabIndex).getPrettyName() +
|
|
"</font> <font color=#999999>" + count + " " + usageLabel + "</font></html>";
|
|
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 + "<font color=#222222><b>" + highlight + "</b></font>" + post;
|
|
line = line.trim();
|
|
|
|
|
|
String text = "<html><font color=#bbbbbb>" +
|
|
(tabLine + 1) + "</font> <font color=#777777>" + line + "</font></html>";
|
|
|
|
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<SimpleName> 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<Integer, List<SketchInterval>> 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<SketchInterval> 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<PreprocessedSketch> 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<DefaultMutableTreeNode> 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<PreprocessedSketch> 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<String, String[]> suggCache =
|
|
JavaMode.importSuggestEnabled ? new HashMap<>() : Collections.emptyMap();
|
|
|
|
final List<Problem> problems = new ArrayList<>();
|
|
|
|
IProblem[] iproblems = ps.compilationUnit.getProblems();
|
|
|
|
{ // Check for curly quotes
|
|
List<JavaProblem> curlyQuoteProblems = checkForCurlyQuotes(ps);
|
|
problems.addAll(curlyQuoteProblems);
|
|
}
|
|
|
|
if (problems.isEmpty()) { // Check for missing braces
|
|
List<JavaProblem> missingBraceProblems = checkForMissingBraces(ps);
|
|
problems.addAll(missingBraceProblems);
|
|
}
|
|
|
|
if (problems.isEmpty()) {
|
|
AtomicReference<ClassPath> searchClassPath = new AtomicReference<>(null);
|
|
List<Problem> 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 -> {
|
|
JavaProblem p = convertIProblem(iproblem, ps);
|
|
|
|
// Handle import suggestions
|
|
if (p != null && 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 JavaProblem convertIProblem(IProblem iproblem, PreprocessedSketch ps) {
|
|
SketchInterval in = ps.mapJavaToSketch(iproblem);
|
|
if (in == SketchInterval.BEFORE_START) return null;
|
|
String badCode = ps.pdeCode.substring(in.startPdeOffset, in.stopPdeOffset);
|
|
int line = ps.tabOffsetToTabLine(in.tabIndex, in.startTabOffset);
|
|
JavaProblem p = JavaProblem.fromIProblem(iproblem, in.tabIndex, line, badCode);
|
|
p.setPDEOffsets(in.startTabOffset, in.stopTabOffset);
|
|
return p;
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
|
|
private static final Pattern CURLY_QUOTE_REGEX =
|
|
Pattern.compile("([“”‘’])", Pattern.UNICODE_CHARACTER_CLASS);
|
|
|
|
static private List<JavaProblem> checkForCurlyQuotes(PreprocessedSketch ps) {
|
|
List<JavaProblem> problems = new ArrayList<>(0);
|
|
|
|
// Go through the scrubbed code and look for curly quotes (they should not be any)
|
|
Matcher matcher = CURLY_QUOTE_REGEX.matcher(ps.scrubbedPdeCode);
|
|
while (matcher.find()) {
|
|
int pdeOffset = matcher.start();
|
|
String q = matcher.group();
|
|
|
|
int tabIndex = ps.pdeOffsetToTabIndex(pdeOffset);
|
|
int tabOffset = ps.pdeOffsetToTabOffset(tabIndex, pdeOffset);
|
|
int tabLine = ps.tabOffsetToTabLine(tabIndex, tabOffset);
|
|
|
|
String message = Language.interpolate("editor.status.bad_curly_quote", q);
|
|
JavaProblem problem = new JavaProblem(message, JavaProblem.ERROR, tabIndex, tabLine);
|
|
problem.setPDEOffsets(tabOffset, tabOffset+1);
|
|
|
|
problems.add(problem);
|
|
}
|
|
|
|
|
|
// Go through iproblems and look for problems involving curly quotes
|
|
List<JavaProblem> problems2 = new ArrayList<>(0);
|
|
IProblem[] iproblems = ps.compilationUnit.getProblems();
|
|
|
|
for (IProblem iproblem : iproblems) {
|
|
switch (iproblem.getID()) {
|
|
case IProblem.ParsingErrorDeleteToken:
|
|
case IProblem.ParsingErrorDeleteTokens:
|
|
case IProblem.ParsingErrorInvalidToken:
|
|
case IProblem.ParsingErrorReplaceTokens:
|
|
case IProblem.UnterminatedString:
|
|
SketchInterval in = ps.mapJavaToSketch(iproblem);
|
|
if (in == SketchInterval.BEFORE_START) continue;
|
|
String badCode = ps.pdeCode.substring(in.startPdeOffset, in.stopPdeOffset);
|
|
matcher.reset(badCode);
|
|
while (matcher.find()) {
|
|
int offset = matcher.start();
|
|
String q = matcher.group();
|
|
int tabStart = in.startTabOffset + offset;
|
|
int tabStop = tabStart + 1;
|
|
// Prevent duplicate problems
|
|
if (problems.stream().noneMatch(p -> p.getStartOffset() == tabStart)) {
|
|
int line = ps.tabOffsetToTabLine(in.tabIndex, tabStart);
|
|
String message;
|
|
if (iproblem.getID() == IProblem.UnterminatedString) {
|
|
message = Language.interpolate("editor.status.unterm_string_curly", q);
|
|
} else {
|
|
message = Language.interpolate("editor.status.bad_curly_quote", q);
|
|
}
|
|
JavaProblem p = new JavaProblem(message, JavaProblem.ERROR, in.tabIndex, line);
|
|
p.setPDEOffsets(tabStart, tabStop);
|
|
problems2.add(p);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
problems.addAll(problems2);
|
|
|
|
return problems;
|
|
}
|
|
|
|
|
|
static private List<JavaProblem> checkForMissingBraces(PreprocessedSketch ps) {
|
|
List<JavaProblem> problems = new ArrayList<>(0);
|
|
for (int tabIndex = 0; tabIndex < ps.tabStartOffsets.length; tabIndex++) {
|
|
int tabStartOffset = ps.tabStartOffsets[tabIndex];
|
|
int tabEndOffset = (tabIndex < ps.tabStartOffsets.length - 1) ?
|
|
ps.tabStartOffsets[tabIndex + 1] : ps.scrubbedPdeCode.length();
|
|
int[] braceResult = SourceUtils.checkForMissingBraces(ps.scrubbedPdeCode, tabStartOffset, tabEndOffset);
|
|
if (braceResult[0] != 0) {
|
|
JavaProblem problem =
|
|
new JavaProblem(braceResult[0] < 0
|
|
? Language.interpolate("editor.status.missing.left_curly_bracket")
|
|
: Language.interpolate("editor.status.missing.right_curly_bracket"),
|
|
JavaProblem.ERROR, tabIndex, braceResult[1]);
|
|
problem.setPDEOffsets(braceResult[3], braceResult[3] + 1);
|
|
problems.add(problem);
|
|
}
|
|
}
|
|
|
|
if (problems.isEmpty()) {
|
|
return problems;
|
|
}
|
|
|
|
int problemTabIndex = problems.get(0).getTabIndex();
|
|
|
|
IProblem missingBraceProblem = Arrays.stream(ps.compilationUnit.getProblems())
|
|
.filter(ErrorChecker::isMissingBraceProblem)
|
|
// 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 -> problemTabIndex == ps.mapJavaToSketch(p).tabIndex)
|
|
.findFirst()
|
|
.orElse(null);
|
|
|
|
// Prefer ECJ problem, shows location more accurately
|
|
if (missingBraceProblem != null) {
|
|
JavaProblem p = convertIProblem(missingBraceProblem, ps);
|
|
if (p != null) {
|
|
problems.clear();
|
|
problems.add(p);
|
|
}
|
|
}
|
|
|
|
return problems;
|
|
}
|
|
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|