diff --git a/java/src/processing/mode/java/pdex/InspectMode.java b/java/src/processing/mode/java/pdex/InspectMode.java new file mode 100644 index 000000000..6624f6866 --- /dev/null +++ b/java/src/processing/mode/java/pdex/InspectMode.java @@ -0,0 +1,200 @@ +package processing.mode.java.pdex; + +import java.awt.EventQueue; +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.function.Predicate; + +import javax.swing.JMenuItem; + +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.IBinding; +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 processing.app.Language; +import processing.app.Messages; +import processing.app.Platform; +import processing.mode.java.JavaEditor; +import processing.mode.java.JavaMode; +import processing.mode.java.pdex.PreprocessedSketch.SketchInterval; + +import static processing.mode.java.pdex.ASTUtils.getSimpleNameAt; +import static processing.mode.java.pdex.ASTUtils.resolveBinding; + + +class InspectMode { + final JavaEditor editor; + final PreprocessingService pps; + final ShowUsage usage; + + 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; + + + InspectMode(JavaEditor editor, PreprocessingService pps, ShowUsage usage) { + this.editor = editor; + this.pps = pps; + this.usage = usage; + + // 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"); + usage.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)) { + usage.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 + } +} \ No newline at end of file diff --git a/java/src/processing/mode/java/pdex/PDEX.java b/java/src/processing/mode/java/pdex/PDEX.java index 6a24c57c8..f0199e9ae 100644 --- a/java/src/processing/mode/java/pdex/PDEX.java +++ b/java/src/processing/mode/java/pdex/PDEX.java @@ -8,33 +8,15 @@ 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.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; @@ -46,48 +28,29 @@ 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 { @@ -112,8 +75,8 @@ public class PDEX { errorChecker = new ErrorChecker(editor, pps); - inspectMode = new InspectMode(editor, pps); showUsage = new ShowUsage(editor, pps); + inspectMode = new InspectMode(editor, pps, showUsage); rename = new Rename(editor, pps, showUsage); if (SHOW_DEBUG_TREE) { debugTree = new DebugTree(editor, pps); @@ -187,729 +150,6 @@ public class PDEX { } - 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; - } - } - - - static private class Rename { - final JavaEditor editor; - final PreprocessingService pps; - final ShowUsage showUsage; - - final JDialog window; - final JTextField textField; - final JLabel oldNameLabel; - - IBinding binding; - PreprocessedSketch ps; - - - Rename(JavaEditor editor, PreprocessingService pps, ShowUsage showUsage) { - this.editor = editor; - this.pps = pps; - this.showUsage = showUsage; - - // 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(Toolkit.zoom(250, 130)); - window.setLayout(new BoxLayout(window.getContentPane(), BoxLayout.Y_AXIS)); - Toolkit.setIcon(window); - - final int b = Toolkit.zoom(5); - - { // Top panel - - // Text field - textField = new JTextField(); - textField.setPreferredSize(Toolkit.zoom(150, 60)); - - // Old name label - oldNameLabel = new JLabel(); - oldNameLabel.setText("Current Name: "); - - // Top panel - JPanel panelTop = new JPanel(); - panelTop.setLayout(new BoxLayout(panelTop, BoxLayout.Y_AXIS)); - panelTop.setBorder(BorderFactory.createEmptyBorder(b, b, b, b)); - panelTop.add(textField); - panelTop.add(Box.createRigidArea(Toolkit.zoom(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 -> { - final String newName = textField.getText().trim(); - if (!newName.isEmpty()) { - if (newName.length() >= 1 && - newName.chars().limit(1).allMatch(Character::isUnicodeIdentifierStart) && - newName.chars().skip(1).allMatch(Character::isUnicodeIdentifierPart)) { - rename(ps, binding, newName); - window.setVisible(false); - } else { - String msg = String.format("'%s' is not a valid name", newName); - JOptionPane.showMessageDialog(editor, msg, "Naming is Hard", - JOptionPane.PLAIN_MESSAGE); - } - } - }); - - JPanel panelBottom = new JPanel(); - panelBottom.setLayout(new BoxLayout(panelBottom, BoxLayout.X_AXIS)); - panelBottom.setBorder(BorderFactory.createEmptyBorder(b, b, b, b)); - panelBottom.add(Box.createHorizontalGlue()); - panelBottom.add(showUsageButton); - panelBottom.add(Box.createRigidArea(Toolkit.zoom(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; diff --git a/java/src/processing/mode/java/pdex/Rename.java b/java/src/processing/mode/java/pdex/Rename.java new file mode 100644 index 000000000..bd44075fd --- /dev/null +++ b/java/src/processing/mode/java/pdex/Rename.java @@ -0,0 +1,308 @@ +package processing.mode.java.pdex; + +import java.awt.EventQueue; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +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.JTextField; +import javax.swing.text.BadLocationException; + +import org.eclipse.jdt.core.dom.ASTNode; +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.SimpleName; + +import processing.app.Language; +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.mode.java.JavaEditor; +import processing.mode.java.pdex.PreprocessedSketch.SketchInterval; + +import static processing.mode.java.pdex.ASTUtils.findAllOccurrences; +import static processing.mode.java.pdex.ASTUtils.getSimpleNameAt; +import static processing.mode.java.pdex.ASTUtils.resolveBinding; + + +class Rename { + final JavaEditor editor; + final PreprocessingService pps; + final ShowUsage showUsage; + + final JDialog window; + final JTextField textField; + final JLabel oldNameLabel; + + IBinding binding; + PreprocessedSketch ps; + + + Rename(JavaEditor editor, PreprocessingService pps, ShowUsage showUsage) { + this.editor = editor; + this.pps = pps; + this.showUsage = showUsage; + + // 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(Toolkit.zoom(250, 130)); + window.setLayout(new BoxLayout(window.getContentPane(), BoxLayout.Y_AXIS)); + Toolkit.setIcon(window); + + final int b = Toolkit.zoom(5); + + { // Top panel + + // Text field + textField = new JTextField(); + textField.setPreferredSize(Toolkit.zoom(150, 60)); + + // Old name label + oldNameLabel = new JLabel(); + oldNameLabel.setText("Current Name: "); + + // Top panel + JPanel panelTop = new JPanel(); + panelTop.setLayout(new BoxLayout(panelTop, BoxLayout.Y_AXIS)); + panelTop.setBorder(BorderFactory.createEmptyBorder(b, b, b, b)); + panelTop.add(textField); + panelTop.add(Box.createRigidArea(Toolkit.zoom(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 -> { + final String newName = textField.getText().trim(); + if (!newName.isEmpty()) { + if (newName.length() >= 1 && + newName.chars().limit(1).allMatch(Character::isUnicodeIdentifierStart) && + newName.chars().skip(1).allMatch(Character::isUnicodeIdentifierPart)) { + rename(ps, binding, newName); + window.setVisible(false); + } else { + String msg = String.format("'%s' is not a valid name", newName); + JOptionPane.showMessageDialog(editor, msg, "Naming is Hard", + JOptionPane.PLAIN_MESSAGE); + } + } + }); + + JPanel panelBottom = new JPanel(); + panelBottom.setLayout(new BoxLayout(panelBottom, BoxLayout.X_AXIS)); + panelBottom.setBorder(BorderFactory.createEmptyBorder(b, b, b, b)); + panelBottom.add(Box.createHorizontalGlue()); + panelBottom.add(showUsageButton); + panelBottom.add(Box.createRigidArea(Toolkit.zoom(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(); + } + } +} \ No newline at end of file diff --git a/java/src/processing/mode/java/pdex/ShowUsage.java b/java/src/processing/mode/java/pdex/ShowUsage.java new file mode 100644 index 000000000..962356238 --- /dev/null +++ b/java/src/processing/mode/java/pdex/ShowUsage.java @@ -0,0 +1,329 @@ +package processing.mode.java.pdex; + +import static processing.mode.java.pdex.ASTUtils.findAllOccurrences; + +import java.awt.Color; +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.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JMenuItem; +import javax.swing.JScrollPane; +import javax.swing.JTree; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreeModel; + +import org.eclipse.jdt.core.dom.IBinding; +import org.eclipse.jdt.core.dom.IMethodBinding; +import org.eclipse.jdt.core.dom.IVariableBinding; +import org.eclipse.jdt.core.dom.SimpleName; + +import processing.app.Language; +import processing.app.ui.EditorStatus; +import processing.app.ui.Toolkit; +import processing.app.ui.ZoomTreeCellRenderer; +import processing.mode.java.JavaEditor; +import processing.mode.java.pdex.PreprocessedSketch.SketchInterval; + + +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(); + } + } +} + + +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; + } +}