package processing.mode.java; import static processing.mode.java.ASTUtils.findAllOccurrences; import static processing.mode.java.ASTUtils.getSimpleNameAt; import static processing.mode.java.ASTUtils.resolveBinding; import java.awt.Dimension; 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.Box; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JRootPane; import javax.swing.JTextField; import javax.swing.WindowConstants; import javax.swing.border.EmptyBorder; 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.Platform; import processing.app.Sketch; import processing.app.SketchCode; import processing.app.syntax.SyntaxDocument; import processing.app.ui.EditorStatus; import processing.app.ui.Toolkit; class Rename { final JavaEditor editor; final PreprocService pps; final ShowUsage showUsage; final JDialog window; final JTextField textField; final JLabel oldNameLabel; IBinding binding; PreprocSketch ps; Rename(JavaEditor editor, PreprocService 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); JRootPane rootPane = window.getRootPane(); window.setTitle("Rename"); window.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE); Toolkit.registerWindowCloseKeys(rootPane, e -> window.setVisible(false)); Toolkit.setIcon(window); window.setModal(true); window.setResizable(false); window.addComponentListener(new ComponentAdapter() { @Override public void componentHidden(ComponentEvent e) { binding = null; ps = null; } }); Box windowBox = Box.createVerticalBox(); windowBox.setBorder(new EmptyBorder(20, 20, 20, 20)); final int GAP = Toolkit.zoom(5); { // old name Box oldBox = Box.createHorizontalBox(); oldNameLabel = new JLabel("Current Name: "); oldBox.add(oldNameLabel); //oldBox.add(Box.createHorizontalStrut(10)); oldBox.add(Box.createHorizontalGlue()); windowBox.add(oldBox); windowBox.add(Box.createVerticalStrut(GAP)); } { // new name Box newBox = Box.createHorizontalBox(); JLabel newNameLabel = new JLabel("New Name: "); newBox.add(newNameLabel); newBox.add(textField = new JTextField(20)); newBox.add(Box.createHorizontalGlue()); windowBox.add(newBox); windowBox.add(Box.createVerticalStrut(GAP*2)); } { // button 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.chars().limit(1).allMatch(Character::isJavaIdentifierStart) && newName.chars().skip(1).allMatch(Character::isJavaIdentifierPart)) { 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); } } }); rootPane.setDefaultButton(renameButton); Box buttonBox = Box.createHorizontalBox(); buttonBox.add(Box.createHorizontalGlue()); buttonBox.add(showUsageButton); if (!Platform.isMacOS()) { buttonBox.add(Box.createHorizontalStrut(GAP)); } buttonBox.add(renameButton); buttonBox.add(Box.createHorizontalGlue()); Dimension showDim = showUsageButton.getPreferredSize(); Dimension renameDim = renameButton.getPreferredSize(); final int niceSize = Math.max(showDim.width, renameDim.width) + GAP; final Dimension buttonDim = new Dimension(niceSize, showDim.height); showUsageButton.setPreferredSize(buttonDim); renameButton.setPreferredSize(buttonDim); windowBox.add(buttonBox); //window.add(panelBottom); } window.add(windowBox); window.pack(); //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(PreprocSketch ps, int tabIndex, int startTabOffset, int stopTabOffset) { if (ps.hasSyntaxErrors) { editor.statusMessage("Cannot rename 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(PreprocSketch 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<>(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.forEach((key, nodes) -> { int tabIndex = key; SketchCode sketchCode = sketch.getCode(tabIndex); SyntaxDocument document = (SyntaxDocument) sketchCode.getDocument(); nodes.stream() // Replace from the end so all unprocessed 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(); } } }