From dfa2845e76fde92f1cab3e955460ba2cc59cd7b5 Mon Sep 17 00:00:00 2001 From: Harsha Indunil Date: Sat, 25 Apr 2026 20:41:16 +0530 Subject: [PATCH 1/4] Add Java reference count hints to the NetBeans editor --- .../editor/lib2/view/ParagraphView.java | 220 ++++++- java/java.editor/nbproject/project.xml | 2 +- .../netbeans/modules/editor/java/JavaKit.java | 2 + .../java/editor/options/Bundle.properties | 3 + .../java/editor/options/InlineHintsPanel.form | 35 + .../java/editor/options/InlineHintsPanel.java | 98 ++- .../editor/options/InlineHintsSettings.java | 18 + .../semantic/HighlightsLayerFactoryImpl.java | 1 + .../semantic/ReferenceCountHintsTask.java | 603 ++++++++++++++++++ .../api/java/source/TreePathHandle.java | 7 +- .../api/java/source/TreePathHandleTest.java | 39 ++ java/refactoring.java/apichanges.xml | 19 + .../org-netbeans-modules-refactoring-java.sig | 8 +- .../java/api/WhereUsedQueryConstants.java | 7 +- .../refactoring/java/api/ui/Bundle.properties | 1 + .../java/api/ui/JavaWhereUsedSupport.java | 346 ++++++++++ .../callhierarchy/CallHierarchyTasks.java | 2 +- .../java/plugins/FindUsagesVisitor.java | 21 +- .../plugins/JavaWhereUsedQueryPlugin.java | 47 +- .../java/ui/PreparedWhereUsedQueryUI.java | 85 +++ .../java/test/FindUsagesFilterTest.java | 67 +- 21 files changed, 1561 insertions(+), 70 deletions(-) create mode 100644 java/java.editor/src/org/netbeans/modules/java/editor/semantic/ReferenceCountHintsTask.java create mode 100644 java/refactoring.java/src/org/netbeans/modules/refactoring/java/api/ui/Bundle.properties create mode 100644 java/refactoring.java/src/org/netbeans/modules/refactoring/java/api/ui/JavaWhereUsedSupport.java create mode 100644 java/refactoring.java/src/org/netbeans/modules/refactoring/java/ui/PreparedWhereUsedQueryUI.java diff --git a/ide/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ParagraphView.java b/ide/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ParagraphView.java index 1362f25277b7..3a76d7f02270 100644 --- a/ide/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ParagraphView.java +++ b/ide/editor.lib2/src/org/netbeans/modules/editor/lib2/view/ParagraphView.java @@ -21,6 +21,8 @@ import java.awt.BasicStroke; import java.awt.Color; +import java.awt.Font; +import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.Shape; @@ -31,11 +33,18 @@ import javax.swing.SwingConstants; import javax.swing.event.DocumentEvent; import javax.swing.text.AttributeSet; +import javax.swing.text.JTextComponent; import javax.swing.text.Position; import javax.swing.text.Position.Bias; import javax.swing.text.View; import javax.swing.text.ViewFactory; import org.netbeans.lib.editor.util.swing.DocumentUtilities; +import org.netbeans.modules.editor.lib2.highlighting.CompoundAttributes; +import org.netbeans.modules.editor.lib2.highlighting.HighlightItem; +import org.netbeans.modules.editor.lib2.highlighting.HighlightsList; +import org.netbeans.modules.editor.lib2.highlighting.HighlightsReader; +import org.netbeans.modules.editor.lib2.highlighting.HighlightingManager; +import org.netbeans.spi.editor.highlighting.HighlightsContainer; /** @@ -52,6 +61,12 @@ public final class ParagraphView extends EditorView implements EditorView.Parent { + private static final String KEY_VIRTUAL_TEXT_BLOCK = "virtual-text-block"; // NOI18N + private static final String KEY_VIRTUAL_TEXT_BLOCK_ANCHOR_OFFSET = "virtual-text-block-anchor-offset"; // NOI18N + private static final String KEY_VIRTUAL_TEXT_BLOCK_TOOLTIP = "virtual-text-block-tooltip"; // NOI18N + private static final int BLOCK_HINT_BOTTOM_GAP = 2; + private static final int BLOCK_HINT_FONT_REDUCTION = 2; + // -J-Dorg.netbeans.modules.editor.lib2.view.ParagraphView.level=FINE private static final Logger LOG = Logger.getLogger(ParagraphView.class.getName()); @@ -113,6 +128,14 @@ public final class ParagraphView extends EditorView implements EditorView.Parent private int statusBits; // 44 + 4 = 48 bytes + private float blockHintHeight; // 48 + 4 = 52 bytes + + private String blockHintText; + + private String blockHintTooltip; + + private int blockHintAnchorOffset = -1; + public ParagraphView(Position startPos) { super(null); setStartPosition(startPos); @@ -252,9 +275,10 @@ boolean updateLayoutAndScheduleRepaint(int pIndex, Shape pAlloc) { Rectangle2D pViewRect = ViewUtils.shapeAsRect(pAlloc); DocumentView docView = getDocumentView(); children.updateLayout(docView, this); + updateBlockHint(docView); boolean spanUpdated = false; float newWidth = children.width(); - float newHeight = children.height(); + float newHeight = children.height() + blockHintHeight; float origWidth = getWidth(); float origHeight = getHeight(); if (newWidth != origWidth) { @@ -306,7 +330,7 @@ public void preferenceChanged(View childView, boolean widthChange, boolean heigh @Override public Shape getChildAllocation(int index, Shape alloc) { checkChildrenNotNull(); - return children.getChildAllocation(index, alloc); + return children.getChildAllocation(index, getContentAllocation(alloc)); } /** @@ -350,26 +374,37 @@ int getLocalOffset(int index) { @Override public int getViewIndexChecked(double x, double y, Shape alloc) { checkChildrenNotNull(); - return children.getViewIndex(this, x, y, alloc); + return children.getViewIndex(this, x, y, getContentAllocation(alloc)); } @Override public Shape modelToViewChecked(int offset, Shape alloc, Bias bias) { checkChildrenNotNull(); - return children.modelToViewChecked(this, offset, alloc, bias); + return children.modelToViewChecked(this, offset, getContentAllocation(alloc), bias); } @Override public int viewToModelChecked(double x, double y, Shape alloc, Bias[] biasReturn) { checkChildrenNotNull(); - return children.viewToModelChecked(this, x, y, alloc, biasReturn); + Rectangle2D hintBounds = getBlockHintBounds(alloc); + if (hintBounds != null && hintBounds.contains(x, y)) { + return getStartOffset(); + } + Shape contentAlloc = getContentAllocation(alloc); + Rectangle2D contentBounds = ViewUtils.shapeAsRect(contentAlloc); + if (y < contentBounds.getY()) { + y = contentBounds.getY(); + } + return children.viewToModelChecked(this, x, y, contentAlloc, biasReturn); } @Override public void paint(Graphics2D g, Shape alloc, Rectangle clipBounds) { // The background is already cleared by BasicTextUI.paintBackground() which uses component.getBackground() checkChildrenNotNull(); - children.paint(this, g, alloc, clipBounds); + Shape contentAlloc = getContentAllocation(alloc); + paintBlockHint(g, alloc, clipBounds); + children.paint(this, g, contentAlloc, clipBounds); if (getDocumentView().op.isGuideLinesEnable()) { DocumentView docView = getDocumentView(); @@ -434,10 +469,11 @@ public void paint(Graphics2D g, Shape alloc, Rectangle clipBounds) { float textsize = docView.op.getDefaultCharWidth() * prefixlength; float tabwidth = docView.op.getDefaultCharWidth() * docView.op.getIndentLevelSize(); int rowHeight = (int) docView.op.getDefaultRowHeight(); + Rectangle contentRect = contentAlloc.getBounds(); if (tabwidth > 0) { - int x = alloc.getBounds().x; - while (x < alloc.getBounds().x + alloc.getBounds().width && x < textsize) { - g.drawLine(x, alloc.getBounds().y, x, alloc.getBounds().y + rowHeight); + int x = contentRect.x; + while (x < contentRect.x + contentRect.width && x < contentRect.x + textsize) { + g.drawLine(x, contentRect.y, x, contentRect.y + rowHeight); x += tabwidth; } } @@ -450,13 +486,31 @@ public void paint(Graphics2D g, Shape alloc, Rectangle clipBounds) { @Override public JComponent getToolTip(double x, double y, Shape allocation) { checkChildrenNotNull(); - return children.getToolTip(this, x, y, allocation); + Rectangle2D hintBounds = getBlockHintBounds(allocation); + if (hintBounds != null && hintBounds.contains(x, y)) { + return null; + } + Shape contentAllocation = getContentAllocation(allocation); + Rectangle2D contentBounds = ViewUtils.shapeAsRect(contentAllocation); + if (y < contentBounds.getY()) { + y = contentBounds.getY(); + } + return children.getToolTip(this, x, y, contentAllocation); } @Override public String getToolTipTextChecked(double x, double y, Shape allocation) { checkChildrenNotNull(); - return children.getToolTipTextChecked(this, x, y, allocation); + Rectangle2D hintBounds = getBlockHintBounds(allocation); + if (hintBounds != null && hintBounds.contains(x, y)) { + return blockHintTooltip; + } + Shape contentAllocation = getContentAllocation(allocation); + Rectangle2D contentBounds = ViewUtils.shapeAsRect(contentAllocation); + if (y < contentBounds.getY()) { + y = contentBounds.getY(); + } + return children.getToolTipTextChecked(this, x, y, contentAllocation); } @Override @@ -564,6 +618,150 @@ private void checkChildrenNotNull() { } } + private Shape getContentAllocation(Shape alloc) { + Rectangle2D.Double bounds = ViewUtils.shape2Bounds(alloc); + if (blockHintHeight > 0f) { + bounds.y += blockHintHeight; + bounds.height = Math.max(0d, bounds.height - blockHintHeight); + } + return bounds; + } + + private void updateBlockHint(DocumentView docView) { + AttributeSet attrs = getParagraphHintAttributes(docView); + String newText = (attrs != null) ? (String) attrs.getAttribute(KEY_VIRTUAL_TEXT_BLOCK) : null; + blockHintText = newText; + blockHintTooltip = (attrs != null) ? (String) attrs.getAttribute(KEY_VIRTUAL_TEXT_BLOCK_TOOLTIP) : null; + Integer anchorOffset = (attrs != null) ? (Integer) attrs.getAttribute(KEY_VIRTUAL_TEXT_BLOCK_ANCHOR_OFFSET) : null; + blockHintAnchorOffset = (anchorOffset != null) ? anchorOffset.intValue() : -1; + if (blockHintText != null) { + JTextComponent textComponent = docView.getTextComponent(); + FontMetrics metrics = getBlockHintMetrics(textComponent, docView); + blockHintHeight = metrics.getHeight() + BLOCK_HINT_BOTTOM_GAP; + } else { + blockHintHeight = 0f; + } + } + + private AttributeSet getParagraphHintAttributes(DocumentView docView) { + JTextComponent textComponent = docView.getTextComponent(); + if (textComponent == null) { + return null; + } + int startOffset = getStartOffset(); + int endOffset = Math.min(docView.getDocument().getLength(), startOffset + 1); + if (endOffset <= startOffset) { + return null; + } + HighlightingManager highlightingManager = HighlightingManager.getInstance(textComponent); + AttributeSet attrs = getParagraphHintAttributes(highlightingManager.getBottomHighlights(), startOffset, endOffset); + if (attrs != null) { + return attrs; + } + return getParagraphHintAttributes(highlightingManager.getTopHighlights(), startOffset, endOffset); + } + + private AttributeSet getParagraphHintAttributes(HighlightsContainer highlightsContainer, int startOffset, int endOffset) { + HighlightsReader reader = new HighlightsReader(highlightsContainer, startOffset, endOffset); + reader.readUntil(endOffset); + HighlightsList highlights = reader.highlightsList(); + if (highlights.size() == 0) { + return null; + } + for (int i = 0; i < highlights.size(); i++) { + AttributeSet attrs = findBlockHintAttributes(highlights.get(i).getAttributes()); + if (attrs != null) { + return attrs; + } + } + return null; + } + + private AttributeSet findBlockHintAttributes(AttributeSet attrs) { + if (attrs == null) { + return null; + } + if (attrs.getAttribute(KEY_VIRTUAL_TEXT_BLOCK) != null) { + return attrs; + } + if (attrs instanceof CompoundAttributes) { + HighlightItem[] items = ((CompoundAttributes) attrs).highlightItems(); + for (HighlightItem item : items) { + AttributeSet nested = item.getAttributes(); + if (nested != null && nested.getAttribute(KEY_VIRTUAL_TEXT_BLOCK) != null) { + return nested; + } + } + } + return null; + } + + private Rectangle2D getBlockHintBounds(Shape allocation) { + if (blockHintText == null || blockHintAnchorOffset < 0) { + return null; + } + Rectangle2D allocBounds = ViewUtils.shapeAsRect(allocation); + int anchorOffset = Math.max(getStartOffset(), Math.min(blockHintAnchorOffset, Math.max(getStartOffset(), getEndOffset() - 1))); + Shape anchorShape = children.modelToViewChecked(this, anchorOffset, getContentAllocation(allocation), Bias.Forward); + Rectangle2D anchorBounds = ViewUtils.shapeAsRect(anchorShape); + JTextComponent textComponent = getDocumentView().getTextComponent(); + if (textComponent == null) { + return null; + } + FontMetrics metrics = getBlockHintMetrics(textComponent, getDocumentView()); + return new Rectangle2D.Double(anchorBounds.getX(), allocBounds.getY(), metrics.stringWidth(blockHintText), blockHintHeight); + } + + private void paintBlockHint(Graphics2D g, Shape alloc, Rectangle clipBounds) { + Rectangle2D hintBounds = getBlockHintBounds(alloc); + if (hintBounds == null || !hintBounds.intersects(clipBounds)) { + return; + } + JTextComponent textComponent = getDocumentView().getTextComponent(); + if (textComponent == null) { + return; + } + Font originalFont = g.getFont(); + Color originalColor = g.getColor(); + Font font = getBlockHintFont(getDocumentView()); + FontMetrics metrics = textComponent.getFontMetrics(font); + g.setFont(font); + g.setColor(getBlockHintColor(textComponent)); + g.drawString(blockHintText, (float) hintBounds.getX(), (float) (hintBounds.getY() + metrics.getAscent())); + g.setFont(originalFont); + g.setColor(originalColor); + } + + private Font getBlockHintFont(DocumentView docView) { + Font defaultFont = docView.op.getDefaultFont(); + float size = Math.max(1f, defaultFont.getSize2D() - BLOCK_HINT_FONT_REDUCTION); + return defaultFont.deriveFont(size); + } + + private FontMetrics getBlockHintMetrics(JTextComponent textComponent, DocumentView docView) { + return textComponent.getFontMetrics(getBlockHintFont(docView)); + } + + private Color getBlockHintColor(JTextComponent textComponent) { + Color foreground = textComponent.getForeground(); + if (foreground == null) { + return Color.GRAY; + } + Color background = textComponent.getBackground(); + if (background == null || foreground.equals(background)) { + return foreground; + } + return blend(foreground, background, 0.65f); + } + + private Color blend(Color foreground, Color background, float foregroundWeight) { + float backgroundWeight = 1f - foregroundWeight; + return new Color( + Math.round(foreground.getRed() * foregroundWeight + background.getRed() * backgroundWeight), + Math.round(foreground.getGreen() * foregroundWeight + background.getGreen() * backgroundWeight), + Math.round(foreground.getBlue() * foregroundWeight + background.getBlue() * backgroundWeight)); + } + /** * Set given status bits to 1. */ diff --git a/java/java.editor/nbproject/project.xml b/java/java.editor/nbproject/project.xml index 75d19165d801..bf0d62ac1e56 100644 --- a/java/java.editor/nbproject/project.xml +++ b/java/java.editor/nbproject/project.xml @@ -319,7 +319,7 @@ 1 - 1.32 + 1.95 diff --git a/java/java.editor/src/org/netbeans/modules/editor/java/JavaKit.java b/java/java.editor/src/org/netbeans/modules/editor/java/JavaKit.java index 918b435865f4..b3bf594f9127 100644 --- a/java/java.editor/src/org/netbeans/modules/editor/java/JavaKit.java +++ b/java/java.editor/src/org/netbeans/modules/editor/java/JavaKit.java @@ -59,6 +59,7 @@ import org.netbeans.modules.java.editor.overridden.GoToSuperTypeAction; import org.netbeans.modules.java.editor.rename.InstantRenameAction; import org.netbeans.modules.java.editor.semantic.GoToMarkOccurrencesAction; +import org.netbeans.modules.java.editor.semantic.ReferenceCountHintsTask; import org.netbeans.spi.editor.typinghooks.DeletedTextInterceptor; import org.netbeans.spi.editor.typinghooks.TypedBreakInterceptor; import org.netbeans.spi.editor.typinghooks.TypedTextInterceptor; @@ -278,6 +279,7 @@ private Action[] removeInstant(Action[] actions) { public void install(JEditorPane c) { super.install(c); ClipboardHandler.install(c); + ReferenceCountHintsTask.install(c); } @EditorActionRegistration(name = generateGoToPopupAction, mimeType = JAVA_MIME_TYPE) diff --git a/java/java.editor/src/org/netbeans/modules/java/editor/options/Bundle.properties b/java/java.editor/src/org/netbeans/modules/java/editor/options/Bundle.properties index ecb363768919..08820c138c15 100644 --- a/java/java.editor/src/org/netbeans/modules/java/editor/options/Bundle.properties +++ b/java/java.editor/src/org/netbeans/modules/java/editor/options/Bundle.properties @@ -152,4 +152,7 @@ CodeCompletionPanel.javadocAutoCompletionTriggersField.text= InlineHintsPanel.javaInlineHintParameterNameCB.text=Show parameter names InlineHintsPanel.javaInlineHintChainedTypesCB.text=Show types of chained methods InlineHintsPanel.javaInlineHintVarTypeCB.text=Show type of var +InlineHintsPanel.javaInlineHintReferenceCountCB.text=Show reference counts +InlineHintsPanel.javaInlineHintReferenceCountMethodsCB.text=Methods +InlineHintsPanel.javaInlineHintReferenceCountTypesCB.text=Types InlineHintsPanel.javaInlineHintsCB.text=Enable Inline Hints diff --git a/java/java.editor/src/org/netbeans/modules/java/editor/options/InlineHintsPanel.form b/java/java.editor/src/org/netbeans/modules/java/editor/options/InlineHintsPanel.form index 8ca2882952e5..fbbb31f1a82e 100644 --- a/java/java.editor/src/org/netbeans/modules/java/editor/options/InlineHintsPanel.form +++ b/java/java.editor/src/org/netbeans/modules/java/editor/options/InlineHintsPanel.form @@ -49,9 +49,17 @@ + + + + + + + + @@ -71,6 +79,12 @@ + + + + + + @@ -98,6 +112,27 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/java/java.editor/src/org/netbeans/modules/java/editor/options/InlineHintsPanel.java b/java/java.editor/src/org/netbeans/modules/java/editor/options/InlineHintsPanel.java index 83df44ed1fa5..14b81bc24e41 100644 --- a/java/java.editor/src/org/netbeans/modules/java/editor/options/InlineHintsPanel.java +++ b/java/java.editor/src/org/netbeans/modules/java/editor/options/InlineHintsPanel.java @@ -38,21 +38,22 @@ */ public class InlineHintsPanel extends javax.swing.JPanel { - public static final String JAVA_INLINE_HINT_PARAMETER_NAME = "javaInlineHintParameterName"; //NOI18N - public static final String JAVA_INLINE_HINT_CHAINED_TYPES = "javaInlineHintChainedTypes"; //NOI18N - public static final String JAVA_INLINE_HINT_VAR_TYPE = "javaInlineHintVarType"; //NOI18N - private static final Map DEFAULT_VALUES; static { Map defaultValuesBuilder = new HashMap<>(); - defaultValuesBuilder.put(JAVA_INLINE_HINT_PARAMETER_NAME, true); - defaultValuesBuilder.put(JAVA_INLINE_HINT_CHAINED_TYPES, false); - defaultValuesBuilder.put(JAVA_INLINE_HINT_VAR_TYPE, false); + defaultValuesBuilder.put(InlineHintsSettings.JAVA_INLINE_HINT_PARAMETER_NAME, true); + defaultValuesBuilder.put(InlineHintsSettings.JAVA_INLINE_HINT_CHAINED_TYPES, false); + defaultValuesBuilder.put(InlineHintsSettings.JAVA_INLINE_HINT_VAR_TYPE, false); + defaultValuesBuilder.put(InlineHintsSettings.JAVA_INLINE_HINT_REFERENCE_COUNT, true); + defaultValuesBuilder.put(InlineHintsSettings.JAVA_INLINE_HINT_REFERENCE_COUNT_METHODS, true); + defaultValuesBuilder.put(InlineHintsSettings.JAVA_INLINE_HINT_REFERENCE_COUNT_TYPES, true); DEFAULT_VALUES = Collections.unmodifiableMap(defaultValuesBuilder); } - private List parameterBoxes; + private List settingsBoxes; + private List inlineHintBoxes; + private List referenceCountBoxes; private InlineHintsOptionsPanelController controller; private boolean changed = false; @@ -69,7 +70,7 @@ public void load(InlineHintsOptionsPanelController controller) { Preferences node = InlineHintsSettings.getCurrentNode(); - for (JCheckBox box : parameterBoxes) { + for (JCheckBox box : settingsBoxes) { box.setSelected(node.getBoolean(box.getActionCommand(), DEFAULT_VALUES.get(box.getActionCommand()))); } @@ -86,7 +87,7 @@ public void store() { Preferences node = InlineHintsSettings.getCurrentNode(); - for (JCheckBox box : parameterBoxes) { + for (JCheckBox box : settingsBoxes) { boolean value = box.isSelected(); boolean original = node.getBoolean(box.getActionCommand(), DEFAULT_VALUES.get(box.getActionCommand())); @@ -127,6 +128,9 @@ private void initComponents() { javaInlineHintParameterNameCB = new javax.swing.JCheckBox(); javaInlineHintChainedTypesCB = new javax.swing.JCheckBox(); javaInlineHintVarTypeCB = new javax.swing.JCheckBox(); + javaInlineHintReferenceCountCB = new javax.swing.JCheckBox(); + javaInlineHintReferenceCountMethodsCB = new javax.swing.JCheckBox(); + javaInlineHintReferenceCountTypesCB = new javax.swing.JCheckBox(); javaInlineHintsCB = new javax.swing.JCheckBox(); setBorder(javax.swing.BorderFactory.createEmptyBorder(8, 8, 8, 8)); @@ -137,6 +141,12 @@ private void initComponents() { org.openide.awt.Mnemonics.setLocalizedText(javaInlineHintVarTypeCB, org.openide.util.NbBundle.getMessage(InlineHintsPanel.class, "InlineHintsPanel.javaInlineHintVarTypeCB.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(javaInlineHintReferenceCountCB, org.openide.util.NbBundle.getMessage(InlineHintsPanel.class, "InlineHintsPanel.javaInlineHintReferenceCountCB.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(javaInlineHintReferenceCountMethodsCB, org.openide.util.NbBundle.getMessage(InlineHintsPanel.class, "InlineHintsPanel.javaInlineHintReferenceCountMethodsCB.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(javaInlineHintReferenceCountTypesCB, org.openide.util.NbBundle.getMessage(InlineHintsPanel.class, "InlineHintsPanel.javaInlineHintReferenceCountTypesCB.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(javaInlineHintsCB, org.openide.util.NbBundle.getMessage(InlineHintsPanel.class, "InlineHintsPanel.javaInlineHintsCB.text")); // NOI18N javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); @@ -149,9 +159,15 @@ private void initComponents() { .addGroup(layout.createSequentialGroup() .addGap(21, 21, 21) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(javaInlineHintReferenceCountCB) .addComponent(javaInlineHintVarTypeCB) .addComponent(javaInlineHintChainedTypesCB) - .addComponent(javaInlineHintParameterNameCB))) + .addComponent(javaInlineHintParameterNameCB) + .addGroup(layout.createSequentialGroup() + .addGap(21, 21, 21) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(javaInlineHintReferenceCountMethodsCB) + .addComponent(javaInlineHintReferenceCountTypesCB))))) .addComponent(javaInlineHintsCB)) .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); @@ -166,16 +182,22 @@ private void initComponents() { .addComponent(javaInlineHintChainedTypesCB) .addGap(6, 6, 6) .addComponent(javaInlineHintVarTypeCB) + .addGap(6, 6, 6) + .addComponent(javaInlineHintReferenceCountCB) + .addGap(6, 6, 6) + .addComponent(javaInlineHintReferenceCountMethodsCB) + .addGap(6, 6, 6) + .addComponent(javaInlineHintReferenceCountTypesCB) .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); }// //GEN-END:initComponents private void updateCheckBoxEnabledState(ActionEvent evt) { - if (javaInlineHintsCB.isSelected() && parameterBoxes.stream().noneMatch(JCheckBox::isSelected)) { + if (javaInlineHintsCB.isSelected() && inlineHintBoxes.stream().noneMatch(JCheckBox::isSelected)) { if (evt != null && evt.getSource() == javaInlineHintsCB) { // restore default if hints are toggled on and no other parameter boxes are selected // this ensures that the view aciton won't become a no-op - for (JCheckBox box : parameterBoxes) { + for (JCheckBox box : inlineHintBoxes) { box.setSelected(DEFAULT_VALUES.get(box.getActionCommand())); } } else { @@ -183,33 +205,59 @@ private void updateCheckBoxEnabledState(ActionEvent evt) { javaInlineHintsCB.setSelected(false); } } - // enable parameter boxes only if inline hints are active - parameterBoxes.forEach(box -> box.setEnabled(javaInlineHintsCB.isSelected())); + inlineHintBoxes.forEach(box -> box.setEnabled(javaInlineHintsCB.isSelected())); + javaInlineHintReferenceCountCB.setEnabled(true); + referenceCountBoxes.forEach(box -> box.setEnabled(javaInlineHintReferenceCountCB.isSelected())); + if (javaInlineHintReferenceCountCB.isSelected() && referenceCountBoxes.stream().noneMatch(JCheckBox::isSelected)) { + if (evt != null && evt.getSource() == javaInlineHintReferenceCountCB) { + for (JCheckBox box : referenceCountBoxes) { + box.setSelected(DEFAULT_VALUES.get(box.getActionCommand())); + } + } else { + javaInlineHintReferenceCountCB.setSelected(false); + referenceCountBoxes.forEach(box -> box.setEnabled(false)); + } + } } // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JCheckBox javaInlineHintChainedTypesCB; private javax.swing.JCheckBox javaInlineHintParameterNameCB; + private javax.swing.JCheckBox javaInlineHintReferenceCountCB; + private javax.swing.JCheckBox javaInlineHintReferenceCountMethodsCB; + private javax.swing.JCheckBox javaInlineHintReferenceCountTypesCB; private javax.swing.JCheckBox javaInlineHintVarTypeCB; private javax.swing.JCheckBox javaInlineHintsCB; // End of variables declaration//GEN-END:variables private void fillBoxes() { - parameterBoxes = new ArrayList<>(); - parameterBoxes.add(javaInlineHintParameterNameCB); - parameterBoxes.add(javaInlineHintChainedTypesCB); - parameterBoxes.add(javaInlineHintVarTypeCB); - - javaInlineHintParameterNameCB.setActionCommand(JAVA_INLINE_HINT_PARAMETER_NAME); - javaInlineHintChainedTypesCB.setActionCommand(JAVA_INLINE_HINT_CHAINED_TYPES); - javaInlineHintVarTypeCB.setActionCommand(JAVA_INLINE_HINT_VAR_TYPE); + inlineHintBoxes = new ArrayList<>(); + inlineHintBoxes.add(javaInlineHintParameterNameCB); + inlineHintBoxes.add(javaInlineHintChainedTypesCB); + inlineHintBoxes.add(javaInlineHintVarTypeCB); + + referenceCountBoxes = new ArrayList<>(); + referenceCountBoxes.add(javaInlineHintReferenceCountMethodsCB); + referenceCountBoxes.add(javaInlineHintReferenceCountTypesCB); + + settingsBoxes = new ArrayList<>(); + settingsBoxes.addAll(inlineHintBoxes); + settingsBoxes.add(javaInlineHintReferenceCountCB); + settingsBoxes.addAll(referenceCountBoxes); + + javaInlineHintParameterNameCB.setActionCommand(InlineHintsSettings.JAVA_INLINE_HINT_PARAMETER_NAME); + javaInlineHintChainedTypesCB.setActionCommand(InlineHintsSettings.JAVA_INLINE_HINT_CHAINED_TYPES); + javaInlineHintVarTypeCB.setActionCommand(InlineHintsSettings.JAVA_INLINE_HINT_VAR_TYPE); + javaInlineHintReferenceCountCB.setActionCommand(InlineHintsSettings.JAVA_INLINE_HINT_REFERENCE_COUNT); + javaInlineHintReferenceCountMethodsCB.setActionCommand(InlineHintsSettings.JAVA_INLINE_HINT_REFERENCE_COUNT_METHODS); + javaInlineHintReferenceCountTypesCB.setActionCommand(InlineHintsSettings.JAVA_INLINE_HINT_REFERENCE_COUNT_TYPES); } private void addListeners() { ActionListener al = e -> checkBoxChanged(e); javaInlineHintsCB.addActionListener(al); - for (JCheckBox box : parameterBoxes) { + for (JCheckBox box : settingsBoxes) { box.addActionListener(al); } } @@ -221,7 +269,7 @@ private void checkBoxChanged(ActionEvent evt) { return; } Preferences node = InlineHintsSettings.getCurrentNode(); - for (JCheckBox box : parameterBoxes) { + for (JCheckBox box : settingsBoxes) { if (node.getBoolean(box.getActionCommand(), DEFAULT_VALUES.get(box.getActionCommand())) != box.isSelected()) { changed = true; return; diff --git a/java/java.editor/src/org/netbeans/modules/java/editor/options/InlineHintsSettings.java b/java/java.editor/src/org/netbeans/modules/java/editor/options/InlineHintsSettings.java index 5d9287ef1c41..ba513abb6368 100644 --- a/java/java.editor/src/org/netbeans/modules/java/editor/options/InlineHintsSettings.java +++ b/java/java.editor/src/org/netbeans/modules/java/editor/options/InlineHintsSettings.java @@ -26,6 +26,12 @@ public class InlineHintsSettings { private static final String INLINE_HINTS = "InlineHints"; // NOI18N + public static final String JAVA_INLINE_HINT_PARAMETER_NAME = "javaInlineHintParameterName"; // NOI18N + public static final String JAVA_INLINE_HINT_CHAINED_TYPES = "javaInlineHintChainedTypes"; // NOI18N + public static final String JAVA_INLINE_HINT_VAR_TYPE = "javaInlineHintVarType"; // NOI18N + public static final String JAVA_INLINE_HINT_REFERENCE_COUNT = "javaInlineHintReferenceCount"; // NOI18N + public static final String JAVA_INLINE_HINT_REFERENCE_COUNT_METHODS = "javaInlineHintReferenceCountMethods"; // NOI18N + public static final String JAVA_INLINE_HINT_REFERENCE_COUNT_TYPES = "javaInlineHintReferenceCountTypes"; // NOI18N // see org.netbeans.modules.editor.actions.ShowInlineHintsAction private static final String JAVA_INLINE_HINTS_KEY = "enable.inline.hints"; // NOI18N @@ -53,6 +59,18 @@ public static void setInlineHintsEnabled(boolean enabled) { getJavaEditorPreferences().putBoolean(JAVA_INLINE_HINTS_KEY, enabled); } + public static boolean isReferenceCountEnabled() { + return getCurrentNode().getBoolean(JAVA_INLINE_HINT_REFERENCE_COUNT, true); + } + + public static boolean isReferenceCountMethodsEnabled() { + return getCurrentNode().getBoolean(JAVA_INLINE_HINT_REFERENCE_COUNT_METHODS, true); + } + + public static boolean isReferenceCountTypesEnabled() { + return getCurrentNode().getBoolean(JAVA_INLINE_HINT_REFERENCE_COUNT_TYPES, true); + } + private static String getCurrentProfileId() { return "default"; // NOI18N } diff --git a/java/java.editor/src/org/netbeans/modules/java/editor/semantic/HighlightsLayerFactoryImpl.java b/java/java.editor/src/org/netbeans/modules/java/editor/semantic/HighlightsLayerFactoryImpl.java index d136c48c7d66..c23266f52c9a 100644 --- a/java/java.editor/src/org/netbeans/modules/java/editor/semantic/HighlightsLayerFactoryImpl.java +++ b/java/java.editor/src/org/netbeans/modules/java/editor/semantic/HighlightsLayerFactoryImpl.java @@ -40,6 +40,7 @@ public HighlightsLayer[] createLayers(Context context) { HighlightsLayer.create(SemanticHighlighter.class.getName() + "-1", ZOrder.SYNTAX_RACK.forPosition(1000), false,semantic), HighlightsLayer.create(SemanticHighlighter.class.getName() + "-2", ZOrder.SYNTAX_RACK.forPosition(1500), false, SemanticHighlighter.getImportHighlightsBag(context.getDocument())), HighlightsLayer.create(SemanticHighlighter.class.getName() + "-3", ZOrder.SYNTAX_RACK.forPosition(1600), false, HighlightsContainers.inlineHintsSettingAwareContainer(context.getDocument(), SemanticHighlighter.getPreTextBag(context.getDocument()))), + HighlightsLayer.create(ReferenceCountHintsTask.class.getName(), ZOrder.SYNTAX_RACK.forPosition(1650), false, ReferenceCountHintsTask.getBag(context.getDocument())), //the mark occurrences layer should be "above" current row and "below" the search layers: HighlightsLayer.create(MarkOccurrencesHighlighter.class.getName(), ZOrder.SHOW_OFF_RACK.forPosition(20), true, MarkOccurrencesHighlighter.getHighlightsBag(context.getDocument())), //"above" mark occurrences, "below" search layers: diff --git a/java/java.editor/src/org/netbeans/modules/java/editor/semantic/ReferenceCountHintsTask.java b/java/java.editor/src/org/netbeans/modules/java/editor/semantic/ReferenceCountHintsTask.java new file mode 100644 index 000000000000..65cfd75f33d9 --- /dev/null +++ b/java/java.editor/src/org/netbeans/modules/java/editor/semantic/ReferenceCountHintsTask.java @@ -0,0 +1,603 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.modules.java.editor.semantic; + +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.Tree; +import com.sun.source.util.TreePath; +import com.sun.source.util.TreePathScanner; +import java.io.IOException; +import java.awt.Font; +import java.awt.FontMetrics; +import java.awt.Shape; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.swing.JEditorPane; +import javax.swing.SwingUtilities; +import javax.swing.text.AttributeSet; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import javax.swing.text.JTextComponent; +import javax.swing.text.Position; +import org.netbeans.api.editor.document.LineDocument; +import org.netbeans.api.editor.document.LineDocumentUtils; +import org.netbeans.api.editor.mimelookup.MimeRegistration; +import org.netbeans.api.editor.settings.AttributesUtilities; +import org.netbeans.api.java.classpath.ClassPath; +import org.netbeans.api.java.source.ClassIndex; +import org.netbeans.api.java.source.ClassIndexListener; +import org.netbeans.api.java.source.CompilationController; +import org.netbeans.api.java.source.CompilationInfo; +import org.netbeans.api.java.source.JavaParserResultTask; +import org.netbeans.api.java.source.JavaSource; +import org.netbeans.api.java.source.SourceUtils; +import org.netbeans.api.java.source.Task; +import org.netbeans.api.java.source.TreePathHandle; +import org.netbeans.modules.java.editor.options.InlineHintsSettings; +import org.netbeans.modules.parsing.spi.Parser; +import org.netbeans.modules.parsing.spi.Scheduler; +import org.netbeans.modules.parsing.spi.SchedulerEvent; +import org.netbeans.modules.parsing.spi.SchedulerTask; +import org.netbeans.modules.parsing.spi.TaskIndexingMode; +import org.netbeans.modules.parsing.spi.TaskFactory; +import org.netbeans.modules.refactoring.api.Scope; +import org.netbeans.modules.refactoring.java.api.ui.JavaWhereUsedSupport; +import org.netbeans.spi.editor.highlighting.HighlightsSequence; +import org.netbeans.spi.editor.highlighting.support.OffsetsBag; +import org.openide.filesystems.FileObject; +import org.openide.util.RequestProcessor; +import org.openide.util.WeakListeners; + +public final class ReferenceCountHintsTask extends JavaParserResultTask { + + static final String KEY_VIRTUAL_TEXT_BLOCK = "virtual-text-block"; // NOI18N + static final String KEY_VIRTUAL_TEXT_BLOCK_ANCHOR_OFFSET = "virtual-text-block-anchor-offset"; // NOI18N + static final String KEY_VIRTUAL_TEXT_BLOCK_TOOLTIP = "virtual-text-block-tooltip"; // NOI18N + + private static final Object KEY_BAG = new Object(); + private static final Object KEY_MANAGER = new Object(); + private static final Object KEY_INTERACTION = new Object(); + private static final int BLOCK_HINT_BOTTOM_GAP = 2; + private static final int BLOCK_HINT_FONT_REDUCTION = 2; + private static final Logger LOG = Logger.getLogger(ReferenceCountHintsTask.class.getName()); + private static final Set DECLARATION_PARENTS = EnumSet.of( + Tree.Kind.CLASS, + Tree.Kind.INTERFACE, + Tree.Kind.ENUM, + Tree.Kind.RECORD, + Tree.Kind.ANNOTATION_TYPE, + Tree.Kind.COMPILATION_UNIT); + + ReferenceCountHintsTask() { + super(JavaSource.Phase.RESOLVED, TaskIndexingMode.ALLOWED_DURING_SCAN); + } + + @Override + public void run(Parser.Result result, SchedulerEvent event) { + CompilationInfo info = CompilationInfo.get(result); + if (info == null) { + return; + } + Document doc = result.getSnapshot().getSource().getDocument(false); + if (doc == null) { + return; + } + Manager manager = getManager(doc); + if (!InlineHintsSettings.isReferenceCountEnabled()) { + manager.clear(); + return; + } + if (SourceUtils.isScanInProgress()) { + manager.defer(info); + return; + } + List declarations = collectDeclarations(info, doc); + manager.update(info, declarations); + } + + @Override + public int getPriority() { + return 110; + } + + @Override + public Class getSchedulerClass() { + return Scheduler.EDITOR_SENSITIVE_TASK_SCHEDULER; + } + + @Override + public void cancel() { + } + + public static OffsetsBag getBag(Document doc) { + OffsetsBag bag = (OffsetsBag) doc.getProperty(KEY_BAG); + if (bag == null) { + bag = new OffsetsBag(doc); + doc.putProperty(KEY_BAG, bag); + } + return bag; + } + + public static void install(JEditorPane pane) { + if (pane.getClientProperty(KEY_INTERACTION) == null) { + HintInteraction interaction = new HintInteraction(pane); + pane.putClientProperty(KEY_INTERACTION, interaction); + pane.addMouseListener(interaction); + pane.addMouseMotionListener(interaction); + } + } + + private static Manager getManager(Document doc) { + Manager manager = (Manager) doc.getProperty(KEY_MANAGER); + if (manager == null) { + manager = new Manager(doc); + doc.putProperty(KEY_MANAGER, manager); + } + return manager; + } + + private static List collectDeclarations(CompilationInfo info, Document doc) { + List declarations = new ArrayList<>(); + new TreePathScanner() { + @Override + public Void visitMethod(MethodTree tree, Void p) { + Element element = info.getTrees().getElement(getCurrentPath()); + if (element != null + && element.getKind() == ElementKind.METHOD + && InlineHintsSettings.isReferenceCountMethodsEnabled()) { + int[] nameSpan = info.getTreeUtilities().findNameSpan(tree); + if (nameSpan != null) { + declarations.add(createDeclaration(doc, info, getCurrentPath(), nameSpan[0], element.getKind())); + } + } + return super.visitMethod(tree, p); + } + + @Override + public Void visitClass(ClassTree tree, Void p) { + Element element = info.getTrees().getElement(getCurrentPath()); + TreePath parentPath = getCurrentPath().getParentPath(); + if (element != null + && InlineHintsSettings.isReferenceCountTypesEnabled() + && isSupportedType(element.getKind()) + && tree.getSimpleName().length() > 0 + && parentPath != null + && DECLARATION_PARENTS.contains(parentPath.getLeaf().getKind())) { + int[] nameSpan = info.getTreeUtilities().findNameSpan(tree); + if (nameSpan != null) { + declarations.add(createDeclaration(doc, info, getCurrentPath(), nameSpan[0], element.getKind())); + } + } + return super.visitClass(tree, p); + } + }.scan(info.getCompilationUnit(), null); + return declarations; + } + + private static boolean isSupportedType(ElementKind kind) { + return kind == ElementKind.CLASS + || kind == ElementKind.INTERFACE + || kind == ElementKind.ENUM + || kind == ElementKind.RECORD + || kind == ElementKind.ANNOTATION_TYPE; + } + + private static Declaration createDeclaration(Document doc, CompilationInfo info, TreePath path, int anchorOffset, ElementKind kind) { + try { + int paragraphOffset = LineDocumentUtils.getLineStart(LineDocumentUtils.asRequired(doc, LineDocument.class), anchorOffset); + return new Declaration( + TreePathHandle.create(path, info), + doc.createPosition(paragraphOffset), + doc.createPosition(anchorOffset), + kind); + } catch (BadLocationException ex) { + throw new IllegalStateException(ex); + } + } + + private static AttributeSet getHintAttributes(Document doc, int offset) { + OffsetsBag bag = getBag(doc); + int endOffset = Math.min(doc.getLength(), offset + 1); + if (endOffset <= offset) { + return null; + } + HighlightsSequence sequence = bag.getHighlights(offset, endOffset); + while (sequence.moveNext()) { + AttributeSet attrs = sequence.getAttributes(); + if (attrs != null && attrs.getAttribute(KEY_VIRTUAL_TEXT_BLOCK) != null) { + return attrs; + } + } + return null; + } + + private static Rectangle2D getHintBounds(JTextComponent component, int paragraphOffset, AttributeSet attrs) throws BadLocationException { + if (attrs == null) { + return null; + } + Integer anchorOffset = (Integer) attrs.getAttribute(KEY_VIRTUAL_TEXT_BLOCK_ANCHOR_OFFSET); + String text = (String) attrs.getAttribute(KEY_VIRTUAL_TEXT_BLOCK); + if (anchorOffset == null || text == null) { + return null; + } + Shape paragraphShape = component.modelToView2D(paragraphOffset); + Shape anchorShape = component.modelToView2D(anchorOffset); + if (paragraphShape == null || anchorShape == null) { + return null; + } + Rectangle2D paragraphBounds = paragraphShape.getBounds2D(); + Rectangle2D anchorBounds = anchorShape.getBounds2D(); + FontMetrics metrics = getHintMetrics(component); + int height = metrics.getHeight() + BLOCK_HINT_BOTTOM_GAP; + return new Rectangle2D.Double(anchorBounds.getX(), paragraphBounds.getY() - height, metrics.stringWidth(text), height); + } + + private static Font getHintFont(JTextComponent component) { + Font font = component.getFont(); + return font.deriveFont(Math.max(1f, font.getSize2D() - BLOCK_HINT_FONT_REDUCTION)); + } + + private static FontMetrics getHintMetrics(JTextComponent component) { + return component.getFontMetrics(getHintFont(component)); + } + + private static String formatCount(int count) { + return count == 1 ? "1 reference" : count + " references"; // NOI18N + } + + @MimeRegistration(mimeType = "text/x-java", service = TaskFactory.class) + public static final class Factory extends TaskFactory { + @Override + public java.util.Collection create(org.netbeans.modules.parsing.api.Snapshot snapshot) { + return Collections.singletonList(new ReferenceCountHintsTask()); + } + } + + private static final class Manager implements ClassIndexListener, Runnable { + + private static final RequestProcessor WORKER = new RequestProcessor(Manager.class); + + private final Document doc; + private final RequestProcessor.Task task; + private final Map cache = new ConcurrentHashMap<>(); + private volatile List declarations = Collections.emptyList(); + private volatile List hintActions = Collections.emptyList(); + private volatile FileObject file; + private volatile Scope scope; + private volatile long serial; + private volatile ClassIndex classIndex; + private volatile AtomicBoolean activeCancel = new AtomicBoolean(); + private volatile Future scanRetry; + + Manager(Document doc) { + this.doc = doc; + this.task = WORKER.create(this); + } + + void update(CompilationInfo info, List declarations) { + FileObject file = info.getFileObject(); + if (file == null) { + clear(); + return; + } + registerIndexListener(info.getClasspathInfo().getClassIndex()); + Scope newScope = createScope(file); + boolean clearCache = !sameDeclarations(this.declarations, declarations) || !sameScope(this.scope, newScope); + this.file = file; + this.scope = newScope; + this.declarations = declarations; + schedule(clearCache); + } + + void defer(CompilationInfo info) { + FileObject file = info.getFileObject(); + if (file == null) { + clear(); + return; + } + registerIndexListener(info.getClasspathInfo().getClassIndex()); + this.file = file; + this.scope = createScope(file); + this.declarations = Collections.emptyList(); + this.hintActions = Collections.emptyList(); + publish(Collections.emptyList(), new OffsetsBag(doc)); + LOG.log(Level.INFO, "Reference count hints deferred for {0} while scan is in progress", file); + scheduleAfterScan(file); + } + + void clear() { + serial++; + activeCancel.set(true); + activeCancel = new AtomicBoolean(); + Future pendingRetry = scanRetry; + if (pendingRetry != null) { + pendingRetry.cancel(false); + scanRetry = null; + } + cache.clear(); + declarations = Collections.emptyList(); + hintActions = Collections.emptyList(); + publish(Collections.emptyList(), new OffsetsBag(doc)); + } + + private void registerIndexListener(ClassIndex newClassIndex) { + if (classIndex == newClassIndex) { + return; + } + classIndex = newClassIndex; + newClassIndex.addClassIndexListener(WeakListeners.create(ClassIndexListener.class, this, newClassIndex)); + cache.clear(); + } + + private Scope createScope(FileObject file) { + ClassPath sourcePath = ClassPath.getClassPath(file, ClassPath.SOURCE); + if (sourcePath != null && sourcePath.getRoots().length > 0) { + return Scope.create(List.of(sourcePath.getRoots()), null, null); + } + return Scope.create(null, null, List.of(file)); + } + + private boolean sameDeclarations(List previous, List current) { + if (previous.size() != current.size()) { + return false; + } + for (int i = 0; i < previous.size(); i++) { + if (!previous.get(i).handle.equals(current.get(i).handle)) { + return false; + } + } + return true; + } + + private boolean sameScope(Scope previous, Scope current) { + return previous != null + && current != null + && previous.getFiles().equals(current.getFiles()) + && previous.getSourceRoots().equals(current.getSourceRoots()) + && previous.isDependencies() == current.isDependencies(); + } + + private void schedule(boolean clearCache) { + if (clearCache) { + cache.clear(); + } + serial++; + activeCancel.set(true); + activeCancel = new AtomicBoolean(); + Future pendingRetry = scanRetry; + if (pendingRetry != null) { + pendingRetry.cancel(false); + scanRetry = null; + } + task.schedule(150); + } + + private void scheduleAfterScan(FileObject file) { + Future pendingRetry = scanRetry; + if (pendingRetry != null && !pendingRetry.isDone()) { + return; + } + JavaSource source = JavaSource.forFileObject(file); + if (source == null) { + schedule(false); + return; + } + try { + scanRetry = source.runWhenScanFinished(new Task() { + @Override + public void run(CompilationController controller) throws Exception { + scanRetry = null; + controller.toPhase(JavaSource.Phase.RESOLVED); + List freshDeclarations = collectDeclarations(controller, doc); + update(controller, freshDeclarations); + } + }, true); + } catch (IOException ex) { + scanRetry = null; + LOG.log(Level.FINE, "Could not schedule reference count refresh after scan for {0}", file); + LOG.log(Level.FINER, null, ex); + schedule(false); + } + } + + @Override + public void run() { + long currentSerial = serial; + Scope currentScope = scope; + List currentDeclarations = declarations; + if (currentScope == null || currentDeclarations.isEmpty()) { + publish(Collections.emptyList(), new OffsetsBag(doc)); + return; + } + AtomicBoolean currentCancel = activeCancel; + OffsetsBag newBag = new OffsetsBag(doc); + List actions = new ArrayList<>(); + List debugCounts = new ArrayList<>(); + for (Declaration declaration : currentDeclarations) { + if (currentCancel.get() || currentSerial != serial) { + return; + } + Integer cachedCount = cache.get(declaration.handle); + int count; + if (cachedCount != null) { + count = cachedCount; + } else { + boolean cacheCount = true; + try { + count = JavaWhereUsedSupport.getDirectReferenceCount(declaration.handle, currentScope, currentCancel); + } catch (Exception ex) { + count = 0; + cacheCount = false; + if (!currentCancel.get()) { + LOG.log(Level.FINE, "Could not compute reference count for {0}", declaration.handle); + LOG.log(Level.FINER, null, ex); + } + } + if (currentCancel.get() || currentSerial != serial) { + return; + } + if (cacheCount) { + cache.put(declaration.handle, count); + } + } + if (count <= 0) { + if (debugCounts.size() < 12) { + debugCounts.add(declaration.handle + "=0"); // NOI18N + } + continue; + } + int paragraphOffset = declaration.paragraphPosition.getOffset(); + if (paragraphOffset >= doc.getLength()) { + continue; + } + int anchorOffset = declaration.anchorPosition.getOffset(); + String label = formatCount(count); + newBag.addHighlight(paragraphOffset, Math.min(doc.getLength(), paragraphOffset + 1), + AttributesUtilities.createImmutable( + KEY_VIRTUAL_TEXT_BLOCK, label, + KEY_VIRTUAL_TEXT_BLOCK_ANCHOR_OFFSET, anchorOffset, + KEY_VIRTUAL_TEXT_BLOCK_TOOLTIP, label)); + actions.add(new HintActionData(declaration.paragraphPosition, declaration.anchorPosition, declaration.handle, currentScope)); + if (debugCounts.size() < 12) { + debugCounts.add(declaration.handle + "=" + count); // NOI18N + } + } + if (currentSerial == serial) { + LOG.log(Level.INFO, "Reference count hints processed for {0}: declarations={1}, published={2}, counts={3}", + new Object[]{file, currentDeclarations.size(), actions.size(), debugCounts}); + publish(actions, newBag); + } + } + + private void publish(List actions, OffsetsBag newBag) { + SwingUtilities.invokeLater(() -> { + hintActions = actions; + getBag(doc).setHighlights(newBag); + }); + } + + private HintActionData findAction(int paragraphOffset) { + for (HintActionData data : hintActions) { + if (data.paragraphPosition.getOffset() == paragraphOffset) { + return data; + } + } + return null; + } + + @Override + public void typesAdded(org.netbeans.api.java.source.TypesEvent event) { + schedule(true); + } + + @Override + public void typesRemoved(org.netbeans.api.java.source.TypesEvent event) { + schedule(true); + } + + @Override + public void typesChanged(org.netbeans.api.java.source.TypesEvent event) { + schedule(true); + } + + @Override + public void rootsAdded(org.netbeans.api.java.source.RootsEvent event) { + schedule(true); + } + + @Override + public void rootsRemoved(org.netbeans.api.java.source.RootsEvent event) { + schedule(true); + } + } + + private static final class HintInteraction extends MouseAdapter { + + private final JEditorPane pane; + + HintInteraction(JEditorPane pane) { + this.pane = pane; + } + + @Override + public void mouseMoved(MouseEvent e) { + pane.setCursor(findAction(e) != null ? java.awt.Cursor.getPredefinedCursor(java.awt.Cursor.HAND_CURSOR) : null); + } + + @Override + public void mouseExited(MouseEvent e) { + pane.setCursor(null); + } + + @Override + public void mouseClicked(MouseEvent e) { + if (!SwingUtilities.isLeftMouseButton(e) || e.getClickCount() != 1 || e.isPopupTrigger()) { + return; + } + HintActionData data = findAction(e); + if (data == null) { + return; + } + e.consume(); + JavaWhereUsedSupport.openDirectReferenceResults(data.handle, data.scope); + } + + private HintActionData findAction(MouseEvent event) { + Document doc = pane.getDocument(); + int offset = pane.viewToModel2D(event.getPoint()); + if (offset < 0) { + return null; + } + AttributeSet attrs = getHintAttributes(doc, offset); + if (attrs == null) { + return null; + } + try { + Rectangle2D bounds = getHintBounds(pane, offset, attrs); + if (bounds == null || !bounds.contains(event.getPoint())) { + return null; + } + } catch (BadLocationException ex) { + return null; + } + return getManager(doc).findAction(offset); + } + } + + private record Declaration(TreePathHandle handle, Position paragraphPosition, Position anchorPosition, ElementKind kind) { + } + + private record HintActionData(Position paragraphPosition, Position anchorPosition, TreePathHandle handle, Scope scope) { + } +} diff --git a/java/java.source.base/src/org/netbeans/api/java/source/TreePathHandle.java b/java/java.source.base/src/org/netbeans/api/java/source/TreePathHandle.java index 14b097f0afb8..e2fe4888dff0 100644 --- a/java/java.source.base/src/org/netbeans/api/java/source/TreePathHandle.java +++ b/java/java.source.base/src/org/netbeans/api/java/source/TreePathHandle.java @@ -525,7 +525,10 @@ public int hashCode() { */ public Element resolveElement(final CompilationInfo info) { if (correspondingEl != null) { - return correspondingEl.resolve(info); + Element resolved = correspondingEl.resolve(info); + if (resolved != null) { + return resolved; + } } if ((this.file != null && info.getFileObject() != null) && info.getFileObject().equals(this.file) && this.position != null) { TreePath tp = this.resolve(info); @@ -869,4 +872,4 @@ public Void scan(Tree node, Void p) { return result; } -} \ No newline at end of file +} diff --git a/java/java.source.base/test/unit/src/org/netbeans/api/java/source/TreePathHandleTest.java b/java/java.source.base/test/unit/src/org/netbeans/api/java/source/TreePathHandleTest.java index 6d762690fcdc..492e5647e592 100644 --- a/java/java.source.base/test/unit/src/org/netbeans/api/java/source/TreePathHandleTest.java +++ b/java/java.source.base/test/unit/src/org/netbeans/api/java/source/TreePathHandleTest.java @@ -414,6 +414,45 @@ public void testResolveElement() throws Exception { assertTrue(tp.getLeaf() == resolved.getLeaf()); assertNotNull(handle.resolveElement(info)); } + + public void testResolveElementFallsBackFromBrokenSignature() throws Exception { + FileObject file = FileUtil.createData(sourceRoot, "test/Test.java"); + String brokenCode = "package test;\n" + + "import other.MissingType;\n" + + "public class Test {\n" + + " public MissingType test() {\n" + + " return null;\n" + + " }\n" + + "}\n"; + + writeIntoFile(file, brokenCode); + + JavaSource js = JavaSource.forFileObject(file); + CompilationInfo brokenInfo = SourceUtilsTestUtil.getCompilationInfo(js, Phase.RESOLVED); + + assertFalse("Expected unresolved type diagnostics", brokenInfo.getDiagnostics().isEmpty()); + + TreePath tp = brokenInfo.getTreeUtilities().pathFor(brokenCode.indexOf("return null")); + while (tp != null && tp.getLeaf().getKind() != Kind.METHOD) { + tp = tp.getParentPath(); + } + + assertNotNull(tp); + + TreePathHandle handle = TreePathHandle.create(tp, brokenInfo); + + writeIntoFile(FileUtil.createData(sourceRoot, "other/MissingType.java"), + "package other;\n" + + "public class MissingType {\n" + + "}\n"); + + SourceUtilsTestUtil.compileRecursively(sourceRoot); + + CompilationInfo fixedInfo = SourceUtilsTestUtil.getCompilationInfo(js, Phase.RESOLVED); + + assertTrue(fixedInfo.getDiagnostics().toString(), fixedInfo.getDiagnostics().isEmpty()); + assertNotNull(handle.resolveElement(fixedInfo)); + } public void testLocVar() throws Exception { FileObject file = FileUtil.createData(sourceRoot, "test/test.java"); diff --git a/java/refactoring.java/apichanges.xml b/java/refactoring.java/apichanges.xml index 659adbd6cb45..96ae63ad68f3 100644 --- a/java/refactoring.java/apichanges.xml +++ b/java/refactoring.java/apichanges.xml @@ -25,6 +25,25 @@ Java Refactoring API + + + Added direct-reference where-used support for Java methods and named types. + + + + + +

+ Added the FIND_DIRECT_REFERENCES query constant and the + JavaWhereUsedSupport utility API to prepare direct-reference + where-used queries, compute reference counts, and open prepared results + without showing the parameter dialog. +

+
+ + + +
Added BINARYFILE, DEPENDENCY, PLATFORM constants to JavaWhereUsedFilters. diff --git a/java/refactoring.java/nbproject/org-netbeans-modules-refactoring-java.sig b/java/refactoring.java/nbproject/org-netbeans-modules-refactoring-java.sig index c844d0cd17d2..c7f517b48d7f 100644 --- a/java/refactoring.java/nbproject/org-netbeans-modules-refactoring-java.sig +++ b/java/refactoring.java/nbproject/org-netbeans-modules-refactoring-java.sig @@ -1,5 +1,5 @@ #Signature file v4.1 -#Version 1.93.0 +#Version 1.95.0 CLSS public abstract interface com.sun.source.doctree.DocTreeVisitor<%0 extends java.lang.Object, %1 extends java.lang.Object> meth public abstract {com.sun.source.doctree.DocTreeVisitor%0} visitAttribute(com.sun.source.doctree.AttributeTree,{com.sun.source.doctree.DocTreeVisitor%1}) @@ -641,6 +641,7 @@ hfds candidateSuperTypes,javaClassHandle,superType hcls TypeMirrorComparator CLSS public final !enum org.netbeans.modules.refactoring.java.api.WhereUsedQueryConstants +fld public final static org.netbeans.modules.refactoring.java.api.WhereUsedQueryConstants FIND_DIRECT_REFERENCES fld public final static org.netbeans.modules.refactoring.java.api.WhereUsedQueryConstants FIND_DIRECT_SUBCLASSES fld public final static org.netbeans.modules.refactoring.java.api.WhereUsedQueryConstants FIND_OVERRIDING_METHODS fld public final static org.netbeans.modules.refactoring.java.api.WhereUsedQueryConstants FIND_SUBCLASSES @@ -669,6 +670,11 @@ cons public init() meth public static org.netbeans.modules.refactoring.api.Scope open(java.lang.String,org.netbeans.modules.refactoring.api.Scope) supr java.lang.Object +CLSS public final org.netbeans.modules.refactoring.java.api.ui.JavaWhereUsedSupport +meth public static int getDirectReferenceCount(org.netbeans.api.java.source.TreePathHandle,org.netbeans.modules.refactoring.api.Scope,java.util.concurrent.atomic.AtomicBoolean) throws java.io.IOException +meth public static void openDirectReferenceResults(org.netbeans.api.java.source.TreePathHandle,org.netbeans.modules.refactoring.api.Scope) +supr java.lang.Object + CLSS public final org.netbeans.modules.refactoring.java.spi.DiffElement meth protected java.lang.String getNewFileContent() meth public java.lang.String getDisplayText() diff --git a/java/refactoring.java/src/org/netbeans/modules/refactoring/java/api/WhereUsedQueryConstants.java b/java/refactoring.java/src/org/netbeans/modules/refactoring/java/api/WhereUsedQueryConstants.java index 07104257d722..d1584debdb0a 100644 --- a/java/refactoring.java/src/org/netbeans/modules/refactoring/java/api/WhereUsedQueryConstants.java +++ b/java/refactoring.java/src/org/netbeans/modules/refactoring/java/api/WhereUsedQueryConstants.java @@ -44,5 +44,10 @@ public enum WhereUsedQueryConstants { * Search from base class * @since 1.45 */ - SEARCH_OVERLOADED; + SEARCH_OVERLOADED, + /** + * Restrict method where-used results to direct references only. + * @since 1.95 + */ + FIND_DIRECT_REFERENCES; } diff --git a/java/refactoring.java/src/org/netbeans/modules/refactoring/java/api/ui/Bundle.properties b/java/refactoring.java/src/org/netbeans/modules/refactoring/java/api/ui/Bundle.properties new file mode 100644 index 000000000000..ac34e7486204 --- /dev/null +++ b/java/refactoring.java/src/org/netbeans/modules/refactoring/java/api/ui/Bundle.properties @@ -0,0 +1 @@ +LBL_UsagesOf=Usages of {0} diff --git a/java/refactoring.java/src/org/netbeans/modules/refactoring/java/api/ui/JavaWhereUsedSupport.java b/java/refactoring.java/src/org/netbeans/modules/refactoring/java/api/ui/JavaWhereUsedSupport.java new file mode 100644 index 000000000000..ab1be0fcf5c3 --- /dev/null +++ b/java/refactoring.java/src/org/netbeans/modules/refactoring/java/api/ui/JavaWhereUsedSupport.java @@ -0,0 +1,346 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.modules.refactoring.java.api.ui; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.text.MessageFormat; +import java.util.EnumSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.swing.SwingUtilities; +import org.netbeans.api.java.source.ClasspathInfo; +import org.netbeans.api.java.source.CompilationController; +import org.netbeans.api.java.source.ElementHandle; +import org.netbeans.api.java.source.JavaSource; +import org.netbeans.api.java.source.SourceUtils; +import org.netbeans.api.java.source.Task; +import org.netbeans.api.java.source.TreePathHandle; +import org.netbeans.modules.refactoring.api.Problem; +import org.netbeans.modules.refactoring.api.RefactoringSession; +import org.netbeans.modules.refactoring.api.Scope; +import org.netbeans.modules.refactoring.api.WhereUsedQuery; +import org.netbeans.modules.refactoring.java.api.WhereUsedQueryConstants; +import org.netbeans.modules.refactoring.java.ui.PreparedWhereUsedQueryUI; +import org.netbeans.modules.refactoring.spi.ui.UI; +import org.openide.filesystems.FileObject; +import org.openide.util.NbBundle; +import org.openide.util.RequestProcessor; +import org.openide.util.lookup.Lookups; + +/** + * Support for prepared Java where-used queries that should open results directly. + * + * @since 1.95 + */ +public final class JavaWhereUsedSupport { + + private static final RequestProcessor WORKER = new RequestProcessor(JavaWhereUsedSupport.class); + + private static final Set SUPPORTED_TYPE_KINDS = EnumSet.of( + ElementKind.CLASS, + ElementKind.INTERFACE, + ElementKind.ENUM, + ElementKind.RECORD, + ElementKind.ANNOTATION_TYPE); + + private JavaWhereUsedSupport() { + } + + /** + * Computes the direct reference count for a Java method or named type. + * + * @param handle the Java declaration handle + * @param scope the search scope, or {@code null} to use the query default + * @param cancel cancellation flag checked between query steps + * @return the number of matching references + * @throws IOException if the declaration cannot be resolved or the query cannot be prepared + */ + public static int getDirectReferenceCount(TreePathHandle handle, Scope scope, AtomicBoolean cancel) throws IOException { + PreparedQuery prepared = prepare(handle, scope, cancel); + if (prepared != null) { + return prepared.session.getRefactoringElements().size(); + } + if (cancel != null && cancel.get()) { + throw new InterruptedIOException("Cancelled"); + } + throw new IOException("Could not prepare direct-reference query for " + handle); // NOI18N + } + + /** + * Computes the direct reference count for a Java method or named type using + * the editor's current classpath info. + * + * @param handle the Java declaration handle + * @param cpInfo the classpath info used to resolve the handle + * @param scope the search scope, or {@code null} to use the query default + * @param cancel cancellation flag checked between query steps + * @return the number of matching references + * @throws IOException if the declaration cannot be resolved or the query cannot be prepared + */ + public static int getDirectReferenceCount(ElementHandle handle, ClasspathInfo cpInfo, Scope scope, AtomicBoolean cancel) throws IOException { + PreparedQuery prepared = prepare(handle, cpInfo, scope, cancel); + if (prepared != null) { + return prepared.session.getRefactoringElements().size(); + } + if (cancel != null && cancel.get()) { + throw new InterruptedIOException("Cancelled"); + } + throw new IOException("Could not prepare direct-reference query for " + handle); // NOI18N + } + + /** + * Opens the direct reference results for a Java method or named type without + * showing the where-used parameter dialog. + * + * @param handle the Java declaration handle + * @param scope the search scope, or {@code null} to use the query default + */ + public static void openDirectReferenceResults(TreePathHandle handle, Scope scope) { + Request.open(handle, scope); + } + + /** + * Opens the direct reference results for a Java method or named type without + * showing the where-used parameter dialog, using the editor's current + * classpath info. + * + * @param handle the Java declaration handle + * @param cpInfo the classpath info used to resolve the handle + * @param scope the search scope, or {@code null} to use the query default + */ + public static void openDirectReferenceResults(ElementHandle handle, ClasspathInfo cpInfo, Scope scope) { + Request.open(handle, cpInfo, scope); + } + + private static PreparedQuery prepare(TreePathHandle handle, Scope scope, AtomicBoolean cancel) throws IOException { + ResolvedElement resolved = resolve(handle); + if (resolved == null) { + return null; + } + WhereUsedQuery query = createQuery(handle, resolved.kind, scope); + RefactoringSession session = RefactoringSession.create(getName(resolved.displayName)); + Problem problem = query.preCheck(); + if (problem != null && problem.isFatal()) { + return null; + } + problem = query.fastCheckParameters(); + if (problem != null && problem.isFatal()) { + return null; + } + problem = query.checkParameters(); + if (problem != null && problem.isFatal()) { + return null; + } + if (cancel != null && cancel.get()) { + return null; + } + problem = query.prepare(session); + if (problem != null && problem.isFatal()) { + return null; + } + if (cancel != null && cancel.get()) { + return null; + } + return new PreparedQuery(query, session, resolved.displayName); + } + + private static PreparedQuery prepare(ElementHandle handle, ClasspathInfo cpInfo, Scope scope, AtomicBoolean cancel) throws IOException { + FileObject file = SourceUtils.getFile(handle, cpInfo); + if (file == null) { + return null; + } + ResolvedElement resolved = resolve(handle, cpInfo, file); + if (resolved == null) { + return null; + } + TreePathHandle treePathHandle = TreePathHandle.from(handle, cpInfo); + WhereUsedQuery query = createQuery(treePathHandle, resolved.kind, scope); + RefactoringSession session = RefactoringSession.create(getName(resolved.displayName)); + Problem problem = query.preCheck(); + if (problem != null && problem.isFatal()) { + return null; + } + problem = query.fastCheckParameters(); + if (problem != null && problem.isFatal()) { + return null; + } + problem = query.checkParameters(); + if (problem != null && problem.isFatal()) { + return null; + } + if (cancel != null && cancel.get()) { + return null; + } + problem = query.prepare(session); + if (problem != null && problem.isFatal()) { + return null; + } + if (cancel != null && cancel.get()) { + return null; + } + return new PreparedQuery(query, session, resolved.displayName); + } + + private static WhereUsedQuery createQuery(TreePathHandle handle, ElementKind kind, Scope scope) { + WhereUsedQuery query = new WhereUsedQuery(Lookups.singleton(handle)); + query.putValue(WhereUsedQuery.SEARCH_IN_COMMENTS, false); + if (scope != null) { + query.getContext().add(scope); + } + if (kind == ElementKind.METHOD) { + query.getContext().add(handle); + query.putValue(WhereUsedQueryConstants.SEARCH_FROM_BASECLASS, false); + query.putValue(WhereUsedQueryConstants.FIND_OVERRIDING_METHODS, false); + query.putValue(WhereUsedQueryConstants.SEARCH_OVERLOADED, false); + query.putValue(WhereUsedQueryConstants.FIND_DIRECT_REFERENCES, true); + query.putValue(WhereUsedQuery.FIND_REFERENCES, true); + } else if (SUPPORTED_TYPE_KINDS.contains(kind)) { + query.putValue(WhereUsedQueryConstants.FIND_SUBCLASSES, false); + query.putValue(WhereUsedQueryConstants.FIND_DIRECT_SUBCLASSES, false); + query.putValue(WhereUsedQuery.FIND_REFERENCES, true); + } else { + throw new IllegalArgumentException("Unsupported where-used element kind: " + kind); // NOI18N + } + return query; + } + + private static ResolvedElement resolve(TreePathHandle handle) throws IOException { + JavaSource source = JavaSource.forFileObject(handle.getFileObject()); + if (source == null) { + return null; + } + final ResolvedElement[] resolved = new ResolvedElement[1]; + source.runUserActionTask(new Task() { + @Override + public void run(CompilationController controller) throws Exception { + controller.toPhase(JavaSource.Phase.ELEMENTS_RESOLVED); + Element element = handle.resolveElement(controller); + if (element == null) { + return; + } + ElementKind kind = element.getKind(); + if (kind == ElementKind.CONSTRUCTOR || !(kind == ElementKind.METHOD || SUPPORTED_TYPE_KINDS.contains(kind))) { + return; + } + String displayName; + if (kind == ElementKind.METHOD) { + displayName = element.getEnclosingElement().getSimpleName() + "." + element.getSimpleName(); // NOI18N + } else { + displayName = element.getSimpleName().toString(); + } + resolved[0] = new ResolvedElement(kind, displayName); + } + }, true); + return resolved[0]; + } + + private static ResolvedElement resolve(ElementHandle handle, ClasspathInfo cpInfo, FileObject file) throws IOException { + JavaSource source = JavaSource.create(cpInfo, file); + if (source == null) { + source = JavaSource.forFileObject(file); + } + if (source == null) { + return null; + } + final ResolvedElement[] resolved = new ResolvedElement[1]; + source.runUserActionTask(new Task() { + @Override + public void run(CompilationController controller) throws Exception { + controller.toPhase(JavaSource.Phase.ELEMENTS_RESOLVED); + Element element = handle.resolve(controller); + if (element == null) { + return; + } + ElementKind kind = element.getKind(); + if (kind == ElementKind.CONSTRUCTOR || !(kind == ElementKind.METHOD || SUPPORTED_TYPE_KINDS.contains(kind))) { + return; + } + String displayName; + if (kind == ElementKind.METHOD) { + displayName = element.getEnclosingElement().getSimpleName() + "." + element.getSimpleName(); // NOI18N + } else { + displayName = element.getSimpleName().toString(); + } + resolved[0] = new ResolvedElement(kind, displayName); + } + }, true); + return resolved[0]; + } + + private static String getName(String displayName) { + return new MessageFormat(NbBundle.getMessage(JavaWhereUsedSupport.class, "LBL_UsagesOf")).format(new Object[]{displayName}); + } + + private record PreparedQuery(WhereUsedQuery query, RefactoringSession session, String displayName) { + } + + private record ResolvedElement(ElementKind kind, String displayName) { + } + + private static final class Request implements Runnable { + + private final TreePathHandle handle; + private final ElementHandle elementHandle; + private final ClasspathInfo cpInfo; + private final Scope scope; + + private Request(TreePathHandle handle, Scope scope) { + this.handle = handle; + this.elementHandle = null; + this.cpInfo = null; + this.scope = scope; + } + + private Request(ElementHandle handle, ClasspathInfo cpInfo, Scope scope) { + this.handle = null; + this.elementHandle = handle; + this.cpInfo = cpInfo; + this.scope = scope; + } + + static void open(TreePathHandle handle, Scope scope) { + WORKER.post(new Request(handle, scope)); + } + + static void open(ElementHandle handle, ClasspathInfo cpInfo, Scope scope) { + WORKER.post(new Request(handle, cpInfo, scope)); + } + + @Override + public void run() { + try { + PreparedQuery prepared = handle != null + ? prepare(handle, scope, new AtomicBoolean()) + : prepare(elementHandle, cpInfo, scope, new AtomicBoolean()); + if (prepared == null) { + return; + } + SwingUtilities.invokeLater(() -> UI.openRefactoringUI( + new PreparedWhereUsedQueryUI(prepared.query, prepared.displayName), + prepared.session, + null)); + } catch (IOException ex) { + // keep the editor interaction lightweight; unresolved handles are silently ignored + } + } + } +} diff --git a/java/refactoring.java/src/org/netbeans/modules/refactoring/java/callhierarchy/CallHierarchyTasks.java b/java/refactoring.java/src/org/netbeans/modules/refactoring/java/callhierarchy/CallHierarchyTasks.java index f85587f91a23..25b4a93e1094 100644 --- a/java/refactoring.java/src/org/netbeans/modules/refactoring/java/callhierarchy/CallHierarchyTasks.java +++ b/java/refactoring.java/src/org/netbeans/modules/refactoring/java/callhierarchy/CallHierarchyTasks.java @@ -329,7 +329,7 @@ public void runTask() throws Exception { Set relevantFiles = null; if (!isCanceled()) { relevantFiles = JavaWhereUsedQueryPlugin.getRelevantFiles( - sourceToQuery, cpInfo, false, false, false, false, true, false, false, null, isCanceled); + sourceToQuery, cpInfo, false, false, false, false, true, false, false, false, null, isCanceled); if (SourceUtils.isScanInProgress()) { elmDesc.setIncomplete(true); } diff --git a/java/refactoring.java/src/org/netbeans/modules/refactoring/java/plugins/FindUsagesVisitor.java b/java/refactoring.java/src/org/netbeans/modules/refactoring/java/plugins/FindUsagesVisitor.java index 2db46bad250f..b38c72906f7d 100644 --- a/java/refactoring.java/src/org/netbeans/modules/refactoring/java/plugins/FindUsagesVisitor.java +++ b/java/refactoring.java/src/org/netbeans/modules/refactoring/java/plugins/FindUsagesVisitor.java @@ -61,16 +61,21 @@ public class FindUsagesVisitor extends ErrorAwareTreePathScanner private boolean usagesInComments; private final AtomicBoolean isCancelled; private List methods; + private final boolean directReferencesOnly; public FindUsagesVisitor(CompilationController workingCopy, AtomicBoolean isCancelled) { - this(workingCopy, isCancelled, false, false); + this(workingCopy, isCancelled, false, false, false); } public FindUsagesVisitor(CompilationController workingCopy, AtomicBoolean isCancelled, boolean findInComments, boolean isSearchOverloadedMethods) { - this(workingCopy, isCancelled, findInComments, isSearchOverloadedMethods, RefactoringUtils.isFromTestRoot(workingCopy.getFileObject(), workingCopy.getClasspathInfo().getClassPath(PathKind.SOURCE)), false, false, new AtomicBoolean()); + this(workingCopy, isCancelled, findInComments, isSearchOverloadedMethods, false); } - public FindUsagesVisitor(CompilationController workingCopy, AtomicBoolean isCancelled, boolean findInComments, boolean isSearchOverloadedMethods, boolean fromTestRoot, boolean fromPlatform, boolean fromDependency, AtomicBoolean inImport) { + public FindUsagesVisitor(CompilationController workingCopy, AtomicBoolean isCancelled, boolean findInComments, boolean isSearchOverloadedMethods, boolean directReferencesOnly) { + this(workingCopy, isCancelled, findInComments, isSearchOverloadedMethods, directReferencesOnly, RefactoringUtils.isFromTestRoot(workingCopy.getFileObject(), workingCopy.getClasspathInfo().getClassPath(PathKind.SOURCE)), false, false, new AtomicBoolean()); + } + + public FindUsagesVisitor(CompilationController workingCopy, AtomicBoolean isCancelled, boolean findInComments, boolean isSearchOverloadedMethods, boolean directReferencesOnly, boolean fromTestRoot, boolean fromPlatform, boolean fromDependency, AtomicBoolean inImport) { try { setWorkingCopy(workingCopy); } catch (ToPhaseException ex) { @@ -84,6 +89,7 @@ public FindUsagesVisitor(CompilationController workingCopy, AtomicBoolean isCanc this.inImport = inImport; this.isCancelled = isCancelled; this.methods = new LinkedList<>(); + this.directReferencesOnly = directReferencesOnly; } @Override @@ -188,9 +194,12 @@ public boolean accept(Element e, TypeMirror type) { } if (elementToFind != null && elementToFind.getKind() == ElementKind.METHOD && el.getKind() == ElementKind.METHOD) { for (ExecutableElement executableElement : methods) { - if (el.equals(executableElement) - || workingCopy.getElements().overrides((ExecutableElement) el, - executableElement, (TypeElement) elementToFind.getEnclosingElement())) { + boolean match = el.equals(executableElement); + if (!match && !directReferencesOnly) { + match = workingCopy.getElements().overrides((ExecutableElement) el, + executableElement, (TypeElement) elementToFind.getEnclosingElement()); + } + if (match) { addUsage(path); } } diff --git a/java/refactoring.java/src/org/netbeans/modules/refactoring/java/plugins/JavaWhereUsedQueryPlugin.java b/java/refactoring.java/src/org/netbeans/modules/refactoring/java/plugins/JavaWhereUsedQueryPlugin.java index 8ea3afd99508..1baa3a162539 100644 --- a/java/refactoring.java/src/org/netbeans/modules/refactoring/java/plugins/JavaWhereUsedQueryPlugin.java +++ b/java/refactoring.java/src/org/netbeans/modules/refactoring/java/plugins/JavaWhereUsedQueryPlugin.java @@ -117,7 +117,7 @@ public static Set getRelevantFiles( final TreePathHandle tph, final ClasspathInfo cpInfo, final boolean isFindSubclasses, final boolean isFindDirectSubclassesOnly, final boolean isFindOverridingMethods, final boolean isSearchOverloadedMethods, - final boolean isFindUsages, final boolean isIncludeDependencies, final boolean isSearchInComments, final Set folders, + final boolean isFindUsages, final boolean isDirectReferencesOnly, final boolean isIncludeDependencies, final boolean isSearchInComments, final Set folders, final AtomicBoolean cancel) { final ClassIndex idx = cpInfo.getClassIndex(); final Set sourceSet = new TreeSet<>(new FileComparator()); @@ -226,22 +226,24 @@ public boolean isDependencies() { sourceSet.addAll(getImplementorsRecursive(idx, cpInfo, enclosingTypeElement, cancel)); } if (isFindUsages) { - //get method references for method and for all it's overriders - Set> s = RefactoringUtils.getImplementorsAsHandles(idx, cpInfo, (TypeElement) method.getEnclosingElement(), cancel); - for (ElementHandle eh : s) { - if (cancel != null && cancel.get()) { - sourceSet.clear(); - return; - } - TypeElement te = eh.resolve(info); - if (te == null) { - continue; - } - for (Element e : te.getEnclosedElements()) { - if (RefactoringUtils.isExecutableElement(e)) { - for (ExecutableElement executableElement : methods) { - if (info.getElements().overrides((ExecutableElement) e, executableElement, te)) { - sourceSet.addAll(idx.getResources(ElementHandle.create(te), EnumSet.of(ClassIndex.SearchKind.METHOD_REFERENCES), searchScopeType, resourceType)); + if (!isDirectReferencesOnly) { + //get method references for method and for all it's overriders + Set> s = RefactoringUtils.getImplementorsAsHandles(idx, cpInfo, (TypeElement) method.getEnclosingElement(), cancel); + for (ElementHandle eh : s) { + if (cancel != null && cancel.get()) { + sourceSet.clear(); + return; + } + TypeElement te = eh.resolve(info); + if (te == null) { + continue; + } + for (Element e : te.getEnclosedElements()) { + if (RefactoringUtils.isExecutableElement(e)) { + for (ExecutableElement executableElement : methods) { + if (info.getElements().overrides((ExecutableElement) e, executableElement, te)) { + sourceSet.addAll(idx.getResources(ElementHandle.create(te), EnumSet.of(ClassIndex.SearchKind.METHOD_REFERENCES), searchScopeType, resourceType)); + } } } } @@ -346,7 +348,7 @@ public Problem prepare(final RefactoringElementsBag elements) { } else { cpath = RefactoringUtils.getClasspathInfoFor(customScope.isDependencies(), customScope.getSourceRoots().toArray(new FileObject[0])); } - Set a = getRelevantFiles(refactoring.getRefactoringSource().lookup(TreePathHandle.class), cpath, isFindSubclasses(), isFindDirectSubclassesOnly(), isFindOverridingMethods(), isSearchOverloadedMethods(), isFindUsages(), customScope.isDependencies(), isSearchInComments(), null, cancelRequested); + Set a = getRelevantFiles(refactoring.getRefactoringSource().lookup(TreePathHandle.class), cpath, isFindSubclasses(), isFindDirectSubclassesOnly(), isFindOverridingMethods(), isSearchOverloadedMethods(), isFindUsages(), isDirectReferencesOnly(), customScope.isDependencies(), isSearchInComments(), null, cancelRequested); fireProgressListenerStep(a.size()); try { @@ -380,7 +382,7 @@ public Problem prepare(final RefactoringElementsBag elements) { } else { cpath = RefactoringUtils.getClasspathInfoFor(customScope.isDependencies(), sourceRoot1); } - Set a = getRelevantFiles(refactoring.getRefactoringSource().lookup(TreePathHandle.class), cpath, isFindSubclasses(), isFindDirectSubclassesOnly(), isFindOverridingMethods(), isSearchOverloadedMethods(), isFindUsages(), customScope.isDependencies(), isSearchInComments(), packages1, cancelRequested); + Set a = getRelevantFiles(refactoring.getRefactoringSource().lookup(TreePathHandle.class), cpath, isFindSubclasses(), isFindDirectSubclassesOnly(), isFindOverridingMethods(), isSearchOverloadedMethods(), isFindUsages(), isDirectReferencesOnly(), customScope.isDependencies(), isSearchInComments(), packages1, cancelRequested); fireProgressListenerStep(a.size()); try { @@ -391,7 +393,7 @@ public Problem prepare(final RefactoringElementsBag elements) { } } } else { - Set a = getRelevantFiles(refactoring.getRefactoringSource().lookup(TreePathHandle.class), cp, isFindSubclasses(), isFindDirectSubclassesOnly(), isFindOverridingMethods(), isSearchOverloadedMethods(), isFindUsages(), false, isSearchInComments(), null, cancelRequested); + Set a = getRelevantFiles(refactoring.getRefactoringSource().lookup(TreePathHandle.class), cp, isFindSubclasses(), isFindDirectSubclassesOnly(), isFindOverridingMethods(), isSearchOverloadedMethods(), isFindUsages(), isDirectReferencesOnly(), false, isSearchInComments(), null, cancelRequested); fireProgressListenerStep(a.size()); try { queryFiles(a, findTask, cp); @@ -469,6 +471,9 @@ private boolean isSearchInComments() { private boolean isSearchFromBaseClass() { return refactoring.getBooleanValue(WhereUsedQueryConstants.SEARCH_FROM_BASECLASS); } + private boolean isDirectReferencesOnly() { + return refactoring.getBooleanValue(WhereUsedQueryConstants.FIND_DIRECT_REFERENCES); + } @Override @NbBundle.Messages({"READ_FILTER=Read filter", "WRITE_FILTER=Write filter", @@ -599,7 +604,7 @@ public void run(CompilationController compiler) throws IOException { AtomicBoolean inImport = new AtomicBoolean(); if (isFindUsages()) { Collection foundElements; - FindUsagesVisitor findVisitor = new FindUsagesVisitor(compiler, cancelled, isSearchInComments(), isSearchOverloadedMethods(), fromTest, fromPlatform, fromDependency, inImport); + FindUsagesVisitor findVisitor = new FindUsagesVisitor(compiler, cancelled, isSearchInComments(), isSearchOverloadedMethods(), isDirectReferencesOnly(), fromTest, fromPlatform, fromDependency, inImport); findVisitor.scan(cu, element); foundElements = findVisitor.getElements(); boolean usagesInComments = findVisitor.usagesInComments(); diff --git a/java/refactoring.java/src/org/netbeans/modules/refactoring/java/ui/PreparedWhereUsedQueryUI.java b/java/refactoring.java/src/org/netbeans/modules/refactoring/java/ui/PreparedWhereUsedQueryUI.java new file mode 100644 index 000000000000..e1aecdb453d5 --- /dev/null +++ b/java/refactoring.java/src/org/netbeans/modules/refactoring/java/ui/PreparedWhereUsedQueryUI.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.modules.refactoring.java.ui; + +import java.text.MessageFormat; +import javax.swing.event.ChangeListener; +import org.netbeans.modules.refactoring.api.AbstractRefactoring; +import org.netbeans.modules.refactoring.api.Problem; +import org.netbeans.modules.refactoring.api.WhereUsedQuery; +import org.netbeans.modules.refactoring.spi.ui.CustomRefactoringPanel; +import org.netbeans.modules.refactoring.spi.ui.RefactoringUI; +import org.openide.util.HelpCtx; +import org.openide.util.NbBundle; + +public final class PreparedWhereUsedQueryUI implements RefactoringUI { + + private final WhereUsedQuery query; + private final String displayName; + + public PreparedWhereUsedQueryUI(WhereUsedQuery query, String displayName) { + this.query = query; + this.displayName = displayName; + } + + @Override + public String getName() { + return new MessageFormat(NbBundle.getMessage(WhereUsedPanel.class, "LBL_UsagesOf")).format(new Object[]{displayName}); + } + + @Override + public String getDescription() { + return new MessageFormat(NbBundle.getMessage(WhereUsedQueryUI.class, "DSC_WhereUsed")).format(new Object[]{displayName}); + } + + @Override + public boolean isQuery() { + return true; + } + + @Override + public CustomRefactoringPanel getPanel(ChangeListener parent) { + return null; + } + + @Override + public Problem setParameters() { + return null; + } + + @Override + public Problem checkParameters() { + return null; + } + + @Override + public boolean hasParameters() { + return false; + } + + @Override + public AbstractRefactoring getRefactoring() { + return query; + } + + @Override + public HelpCtx getHelpCtx() { + return new HelpCtx("org.netbeans.modules.refactoring.java.ui.WhereUsedQueryUI"); // NOI18N + } +} diff --git a/java/refactoring.java/test/unit/src/org/netbeans/modules/refactoring/java/test/FindUsagesFilterTest.java b/java/refactoring.java/test/unit/src/org/netbeans/modules/refactoring/java/test/FindUsagesFilterTest.java index 3c6490f2dee1..a5e695aca2af 100644 --- a/java/refactoring.java/test/unit/src/org/netbeans/modules/refactoring/java/test/FindUsagesFilterTest.java +++ b/java/refactoring.java/test/unit/src/org/netbeans/modules/refactoring/java/test/FindUsagesFilterTest.java @@ -20,6 +20,7 @@ import com.sun.source.tree.CompilationUnitTree; import com.sun.source.util.TreePath; +import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.concurrent.atomic.AtomicBoolean; @@ -27,8 +28,11 @@ import org.netbeans.api.java.source.CompilationController; import org.netbeans.api.java.source.JavaSource; import org.netbeans.api.java.source.Task; +import org.netbeans.api.java.source.TreePathHandle; import org.netbeans.modules.java.source.parsing.JavacParser; +import org.netbeans.modules.refactoring.api.Scope; import org.netbeans.modules.refactoring.java.WhereUsedElement; +import org.netbeans.modules.refactoring.java.api.ui.JavaWhereUsedSupport; import org.netbeans.modules.refactoring.java.plugins.FindUsagesVisitor; import org.netbeans.modules.refactoring.java.spi.JavaWhereUsedFilters; import static org.netbeans.modules.refactoring.java.test.RefactoringTestBase.writeFilesAndWaitForScan; @@ -227,10 +231,71 @@ public void testArrayReadWrite() throws Exception { Pair.of("lijst[4] = lijst[5];", JavaWhereUsedFilters.ReadWrite.READ), Pair.of("lijst = null;", JavaWhereUsedFilters.ReadWrite.WRITE)); } + + public void testDirectMethodReferencesIgnoreOverrides() throws Exception { + String source; + writeFilesAndWaitForScan(src, new RefactoringTestBase.File("t/A.java", source = "package t;\n" + + "interface DirectBase {\n" + + " void ping();\n" + + "}\n" + + "class DirectImpl implements DirectBase {\n" + + " @Override public void ping() {}\n" + + "}\n" + + "public class A {\n" + + " void invoke() {\n" + + " DirectBase base = new DirectImpl();\n" + + " base.ping();\n" + + " new DirectImpl().ping();\n" + + " }\n" + + "}\n")); + + performFind(src.getFileObject("t/A.java"), source.indexOf("void ping") + 6, false, false, false, false, + Pair.of("base.ping();", (JavaWhereUsedFilters.ReadWrite) null), + Pair.of("new DirectImpl().ping();", (JavaWhereUsedFilters.ReadWrite) null)); + performFind(src.getFileObject("t/A.java"), source.indexOf("void ping") + 6, false, false, false, true, + Pair.of("base.ping();", (JavaWhereUsedFilters.ReadWrite) null)); + } + + public void testDirectMethodReferenceCount() throws Exception { + String source; + writeFilesAndWaitForScan(src, new RefactoringTestBase.File("t/A.java", source = "package t;\n" + + "interface DirectBase {\n" + + " void ping();\n" + + "}\n" + + "class DirectImpl implements DirectBase {\n" + + " @Override public void ping() {}\n" + + "}\n" + + "public class A {\n" + + " void invoke() {\n" + + " DirectBase base = new DirectImpl();\n" + + " base.ping();\n" + + " new DirectImpl().ping();\n" + + " }\n" + + "}\n")); + final TreePathHandle[] handle = new TreePathHandle[1]; + JavaSource.forFileObject(src.getFileObject("t/A.java")).runUserActionTask(new Task() { + + @Override + public void run(CompilationController javac) throws Exception { + javac.toPhase(JavaSource.Phase.RESOLVED); + TreePath tp = javac.getTreeUtilities().pathFor(source.indexOf("void ping") + 6); + handle[0] = TreePathHandle.create(tp, javac); + } + }, true); + + assertEquals(1, JavaWhereUsedSupport.getDirectReferenceCount(handle[0], Scope.create(Arrays.asList(src), null, null), new AtomicBoolean())); + } @SuppressWarnings("null") private void performFind(FileObject source, final int absPos, final boolean searchInComments, boolean inImport, boolean inComment, Pair... expected) throws Exception { + performFind(source, absPos, searchInComments, inImport, inComment, false, expected); + } + + @SuppressWarnings("null") + private void performFind(FileObject source, final int absPos, final boolean searchInComments, + boolean inImport, boolean inComment, final boolean directReferencesOnly, + Pair... expected) throws Exception { final FindUsagesVisitor[] r = new FindUsagesVisitor[1]; JavaSource.forFileObject(source).runUserActionTask(new Task() { @@ -243,7 +308,7 @@ public void run(CompilationController javac) throws Exception { Element el = javac.getTrees().getElement(tp); AtomicBoolean isCancelled = new AtomicBoolean(); AtomicBoolean inImport = new AtomicBoolean(); - r[0] = new FindUsagesVisitor(javac, isCancelled, searchInComments, false, false, false, false, inImport); + r[0] = new FindUsagesVisitor(javac, isCancelled, searchInComments, false, directReferencesOnly, false, false, false, inImport); r[0].scan(cut, el); } }, true); From 7f141cdef790fcedd83ebaf63baa8e7c5c5ca354 Mon Sep 17 00:00:00 2001 From: Harsha Indunil Date: Sun, 29 Mar 2026 08:55:53 +0530 Subject: [PATCH 2/4] Show reference usages popup from count hints --- .../semantic/ReferenceCountHintsTask.java | 19 +- .../editor/semantic/ReferenceUsagesPopup.java | 620 ++++++++++++++++++ 2 files changed, 633 insertions(+), 6 deletions(-) create mode 100644 java/java.editor/src/org/netbeans/modules/java/editor/semantic/ReferenceUsagesPopup.java diff --git a/java/java.editor/src/org/netbeans/modules/java/editor/semantic/ReferenceCountHintsTask.java b/java/java.editor/src/org/netbeans/modules/java/editor/semantic/ReferenceCountHintsTask.java index 65cfd75f33d9..9f4e84f3bb9e 100644 --- a/java/java.editor/src/org/netbeans/modules/java/editor/semantic/ReferenceCountHintsTask.java +++ b/java/java.editor/src/org/netbeans/modules/java/editor/semantic/ReferenceCountHintsTask.java @@ -552,7 +552,7 @@ private static final class HintInteraction extends MouseAdapter { @Override public void mouseMoved(MouseEvent e) { - pane.setCursor(findAction(e) != null ? java.awt.Cursor.getPredefinedCursor(java.awt.Cursor.HAND_CURSOR) : null); + pane.setCursor(findMatch(e) != null ? java.awt.Cursor.getPredefinedCursor(java.awt.Cursor.HAND_CURSOR) : null); } @Override @@ -565,15 +565,15 @@ public void mouseClicked(MouseEvent e) { if (!SwingUtilities.isLeftMouseButton(e) || e.getClickCount() != 1 || e.isPopupTrigger()) { return; } - HintActionData data = findAction(e); - if (data == null) { + HintMatch match = findMatch(e); + if (match == null) { return; } e.consume(); - JavaWhereUsedSupport.openDirectReferenceResults(data.handle, data.scope); + ReferenceUsagesPopup.show(pane, match.bounds, match.data.handle, match.data.scope, match.label); } - private HintActionData findAction(MouseEvent event) { + private HintMatch findMatch(MouseEvent event) { Document doc = pane.getDocument(); int offset = pane.viewToModel2D(event.getPoint()); if (offset < 0) { @@ -588,10 +588,14 @@ private HintActionData findAction(MouseEvent event) { if (bounds == null || !bounds.contains(event.getPoint())) { return null; } + HintActionData data = getManager(doc).findAction(offset); + if (data == null) { + return null; + } + return new HintMatch(data, bounds, (String) attrs.getAttribute(KEY_VIRTUAL_TEXT_BLOCK)); } catch (BadLocationException ex) { return null; } - return getManager(doc).findAction(offset); } } @@ -600,4 +604,7 @@ private record Declaration(TreePathHandle handle, Position paragraphPosition, Po private record HintActionData(Position paragraphPosition, Position anchorPosition, TreePathHandle handle, Scope scope) { } + + private record HintMatch(HintActionData data, Rectangle2D bounds, String label) { + } } diff --git a/java/java.editor/src/org/netbeans/modules/java/editor/semantic/ReferenceUsagesPopup.java b/java/java.editor/src/org/netbeans/modules/java/editor/semantic/ReferenceUsagesPopup.java new file mode 100644 index 000000000000..6baa10719274 --- /dev/null +++ b/java/java.editor/src/org/netbeans/modules/java/editor/semantic/ReferenceUsagesPopup.java @@ -0,0 +1,620 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.modules.java.editor.semantic; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.swing.DefaultListCellRenderer; +import javax.swing.DefaultListModel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.ListSelectionModel; +import javax.swing.ScrollPaneConstants; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.text.JTextComponent; +import org.netbeans.api.java.source.CompilationController; +import org.netbeans.api.java.source.JavaSource; +import org.netbeans.api.java.source.Task; +import org.netbeans.api.java.source.TreePathHandle; +import org.netbeans.modules.java.editor.overridden.PopupUtil; +import org.netbeans.modules.refactoring.api.Problem; +import org.netbeans.modules.refactoring.api.RefactoringElement; +import org.netbeans.modules.refactoring.api.RefactoringSession; +import org.netbeans.modules.refactoring.api.Scope; +import org.netbeans.modules.refactoring.api.WhereUsedQuery; +import org.netbeans.modules.refactoring.java.api.WhereUsedQueryConstants; +import org.netbeans.modules.refactoring.java.api.ui.JavaWhereUsedSupport; +import org.netbeans.spi.editor.completion.support.CompletionUtilities; +import org.openide.filesystems.FileObject; +import org.openide.text.PositionBounds; +import org.openide.util.NbBundle; +import org.openide.util.RequestProcessor; +import org.openide.util.lookup.Lookups; + +@NbBundle.Messages({ + "LBL_ReferencePopup_Loading=Searching references...", + "LBL_ReferencePopup_NoReferences=No references found", + "LBL_ReferencePopup_ShowAll=Show All References...", + "LBL_ReferencePopup_Error=Unable to load references" +}) +final class ReferenceUsagesPopup extends JPanel implements FocusListener { + + private static final RequestProcessor WORKER = new RequestProcessor(ReferenceUsagesPopup.class); + private static final int MAX_VISIBLE_ROWS = 12; + private static final int DEFAULT_VISIBLE_ROWS = 8; + private static final int POPUP_WIDTH = 560; + + private final ReferenceLoader loader; + private final AtomicBoolean cancel = new AtomicBoolean(); + private final DefaultListModel model = new DefaultListModel<>(); + private final JList list = new JList<>(model) { + @Override + public boolean getScrollableTracksViewportWidth() { + return true; + } + }; + + private ReferenceUsagesPopup(TreePathHandle handle, Scope scope, String accessibleName) { + this.loader = new ReferenceLoader(handle, scope); + + setLayout(new BorderLayout()); + setOpaque(true); + setFocusCycleRoot(true); + if (accessibleName != null) { + getAccessibleContext().setAccessibleName(accessibleName); + } + + configureList(); + setBackground(list.getBackground()); + add(createScrollPane(), BorderLayout.CENTER); + setEntries(List.of(new MessageEntry(Bundle.LBL_ReferencePopup_Loading()))); + addFocusListener(this); + } + + static void show(JTextComponent component, Rectangle2D hintBounds, TreePathHandle handle, Scope scope, String label) { + PopupUtil.hidePopup(); + ReferenceUsagesPopup popup = new ReferenceUsagesPopup(handle, scope, label); + Point location = popupLocation(component, hintBounds); + PopupUtil.showPopup(popup, null, location.x, location.y + 1, true, (int) Math.ceil(hintBounds.getHeight())); + popup.loadReferences(); + } + + @Override + public void focusGained(FocusEvent e) { + list.requestFocusInWindow(); + } + + @Override + public void focusLost(FocusEvent e) { + } + + private void loadReferences() { + WORKER.post(() -> { + List entries = loader.load(cancel); + if (entries == null || cancel.get()) { + return; + } + SwingUtilities.invokeLater(() -> { + if (!cancel.get()) { + setEntries(entries); + } + }); + }); + } + + private void setEntries(List entries) { + model.clear(); + for (PopupEntry entry : entries) { + model.addElement(entry); + } + list.setVisibleRowCount(Math.min(Math.max(entries.size(), 1), MAX_VISIBLE_ROWS)); + selectFirstSelectable(); + } + + private void selectFirstSelectable() { + for (int i = 0; i < model.size(); i++) { + if (model.get(i).selectable()) { + list.setSelectedIndex(i); + return; + } + } + list.clearSelection(); + } + + private void activateSelection() { + PopupEntry entry = list.getSelectedValue(); + if (entry == null || !entry.selectable()) { + return; + } + cancel.set(true); + PopupUtil.hidePopup(); + entry.activate(); + } + + private void configureList() { + list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + list.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + list.setCellRenderer(new Renderer()); + list.setFixedCellHeight(Math.max(list.getFontMetrics(list.getFont()).getHeight() + 8, 20)); + list.setVisibleRowCount(DEFAULT_VISIBLE_ROWS); + list.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 1) { + activateSelection(); + } + } + }); + list.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ENTER && e.getModifiersEx() == 0) { + activateSelection(); + e.consume(); + } + } + }); + } + + private JScrollPane createScrollPane() { + JScrollPane scrollPane = new JScrollPane(list); + scrollPane.setBorder(null); + scrollPane.setBackground(list.getBackground()); + scrollPane.getViewport().setBackground(list.getBackground()); + scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + scrollPane.setPreferredSize(new Dimension(POPUP_WIDTH, list.getFixedCellHeight() * DEFAULT_VISIBLE_ROWS)); + return scrollPane; + } + + private static Point popupLocation(JTextComponent component, Rectangle2D hintBounds) { + Point location = new Point((int) Math.round(hintBounds.getX()), (int) Math.round(hintBounds.getMaxY())); + SwingUtilities.convertPointToScreen(location, component); + return location; + } + + private sealed interface PopupEntry permits ReferenceEntry, ActionEntry, MessageEntry { + + String leftHtml(); + + String rightText(); + + String tooltip(); + + boolean selectable(); + + void activate(); + } + + private record ReferenceEntry(RefactoringElement element) implements PopupEntry { + + @Override + public String leftHtml() { + return ReferencePresentation.displayText(element); + } + + @Override + public String rightText() { + return ReferencePresentation.locationText(element); + } + + @Override + public String tooltip() { + return ReferencePresentation.tooltipText(element); + } + + @Override + public boolean selectable() { + return true; + } + + @Override + public void activate() { + element.openInEditor(); + } + } + + private record ActionEntry(String text, Runnable action) implements PopupEntry { + + @Override + public String leftHtml() { + return text; + } + + @Override + public String rightText() { + return null; + } + + @Override + public String tooltip() { + return text; + } + + @Override + public boolean selectable() { + return true; + } + + @Override + public void activate() { + action.run(); + } + } + + private record MessageEntry(String text) implements PopupEntry { + + @Override + public String leftHtml() { + return text; + } + + @Override + public String rightText() { + return null; + } + + @Override + public String tooltip() { + return text; + } + + @Override + public boolean selectable() { + return false; + } + + @Override + public void activate() { + } + } + + private static final class Renderer extends DefaultListCellRenderer { + + private static final int ALTERNATE_ROW_DELTA = 6; + private static final double MIN_CONTRAST_RATIO = 3.0d; + private PopupEntry entry; + + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + entry = (PopupEntry) value; + setToolTipText(entry.tooltip()); + Color background = isSelected ? list.getSelectionBackground() : backgroundForRow(list, index); + Color foreground = isSelected + ? list.getSelectionForeground() + : foregroundForEntry(entry, list.getForeground(), background); + setBackground(background); + setForeground(foreground); + return this; + } + + @Override + protected void paintComponent(Graphics g) { + Color bgColor = getBackground(); + Color fgColor = getForeground(); + + g.setColor(bgColor); + g.fillRect(0, 0, getWidth(), getHeight()); + CompletionUtilities.renderHtml(null, entry.leftHtml(), entry.rightText(), g, getFont(), fgColor, getWidth(), getHeight(), false); + } + + private Color foregroundForEntry(PopupEntry entry, Color defaultColor, Color background) { + if (entry instanceof MessageEntry) { + Color disabled = UIManager.getColor("Label.disabledForeground"); // NOI18N + return readableColor(disabled, background, defaultColor); + } + if (entry instanceof ActionEntry) { + Color link = UIManager.getColor("nb.html.link.foreground"); // NOI18N + return readableColor(link, background, defaultColor); + } + return defaultColor; + } + + private Color backgroundForRow(JList list, int index) { + if ((index % 2) != 0) { + return list.getBackground(); + } + Color alternate = UIManager.getColor("Table.alternateRowColor"); // NOI18N + if (alternate != null && !alternate.equals(list.getBackground())) { + return alternate; + } + return shiftBrightness(list.getBackground(), isDark(list.getBackground()) ? ALTERNATE_ROW_DELTA : -ALTERNATE_ROW_DELTA); + } + + private Color readableColor(Color candidate, Color background, Color fallback) { + Color resolved = candidate != null ? candidate : fallback; + if (contrastRatio(resolved, background) >= MIN_CONTRAST_RATIO) { + return resolved; + } + return fallback; + } + + private static Color shiftBrightness(Color color, int delta) { + return new Color( + clamp(color.getRed() + delta), + clamp(color.getGreen() + delta), + clamp(color.getBlue() + delta)); + } + + private static int clamp(int value) { + return Math.max(0, Math.min(255, value)); + } + + private static boolean isDark(Color color) { + return relativeLuminance(color) < 0.5d; + } + + private static double contrastRatio(Color first, Color second) { + double firstLuminance = relativeLuminance(first) + 0.05d; + double secondLuminance = relativeLuminance(second) + 0.05d; + return Math.max(firstLuminance, secondLuminance) / Math.min(firstLuminance, secondLuminance); + } + + private static double relativeLuminance(Color color) { + return 0.2126d * linearize(color.getRed() / 255d) + + 0.7152d * linearize(color.getGreen() / 255d) + + 0.0722d * linearize(color.getBlue() / 255d); + } + + private static double linearize(double component) { + return component <= 0.03928d + ? component / 12.92d + : Math.pow((component + 0.055d) / 1.055d, 2.4d); + } + } + + private static final class ReferenceLoader { + + private static final Set SUPPORTED_TYPE_KINDS = EnumSet.of( + ElementKind.CLASS, + ElementKind.INTERFACE, + ElementKind.ENUM, + ElementKind.RECORD, + ElementKind.ANNOTATION_TYPE); + + private final TreePathHandle handle; + private final Scope scope; + + ReferenceLoader(TreePathHandle handle, Scope scope) { + this.handle = handle; + this.scope = scope; + } + + List load(AtomicBoolean cancel) { + try { + List references = findDirectReferences(cancel); + checkCancelled(cancel); + return buildEntries(references); + } catch (InterruptedIOException ex) { + return null; + } catch (IOException ex) { + return List.of( + new MessageEntry(Bundle.LBL_ReferencePopup_Error()), + new ActionEntry(Bundle.LBL_ReferencePopup_ShowAll(), this::openAllReferences)); + } + } + + private List buildEntries(List references) { + if (references.isEmpty()) { + return List.of(new MessageEntry(Bundle.LBL_ReferencePopup_NoReferences())); + } + List entries = new ArrayList<>(references.size() + 1); + for (RefactoringElement reference : references) { + entries.add(new ReferenceEntry(reference)); + } + entries.add(new ActionEntry(Bundle.LBL_ReferencePopup_ShowAll(), this::openAllReferences)); + return entries; + } + + private void openAllReferences() { + JavaWhereUsedSupport.openDirectReferenceResults(handle, scope); + } + + private List findDirectReferences(AtomicBoolean cancel) throws IOException { + ResolvedElement resolved = resolve(); + if (resolved == null) { + return Collections.emptyList(); + } + WhereUsedQuery query = createQuery(resolved.kind); + RefactoringSession session = RefactoringSession.create(resolved.displayName); + Problem problem = query.preCheck(); + if (isFatal(problem)) { + return Collections.emptyList(); + } + problem = query.fastCheckParameters(); + if (isFatal(problem)) { + return Collections.emptyList(); + } + problem = query.checkParameters(); + if (isFatal(problem)) { + return Collections.emptyList(); + } + checkCancelled(cancel); + problem = query.prepare(session); + if (isFatal(problem)) { + return Collections.emptyList(); + } + checkCancelled(cancel); + List references = new ArrayList<>(session.getRefactoringElements()); + references.sort(ReferencePresentation.REFERENCE_ORDER); + return references; + } + + private WhereUsedQuery createQuery(ElementKind kind) { + WhereUsedQuery query = new WhereUsedQuery(Lookups.singleton(handle)); + query.putValue(WhereUsedQuery.SEARCH_IN_COMMENTS, false); + if (scope != null) { + query.getContext().add(scope); + } + if (kind == ElementKind.METHOD) { + query.getContext().add(handle); + query.putValue(WhereUsedQueryConstants.SEARCH_FROM_BASECLASS, false); + query.putValue(WhereUsedQueryConstants.FIND_OVERRIDING_METHODS, false); + query.putValue(WhereUsedQueryConstants.SEARCH_OVERLOADED, false); + query.putValue(WhereUsedQueryConstants.FIND_DIRECT_REFERENCES, true); + query.putValue(WhereUsedQuery.FIND_REFERENCES, true); + } else if (SUPPORTED_TYPE_KINDS.contains(kind)) { + query.putValue(WhereUsedQueryConstants.FIND_SUBCLASSES, false); + query.putValue(WhereUsedQueryConstants.FIND_DIRECT_SUBCLASSES, false); + query.putValue(WhereUsedQuery.FIND_REFERENCES, true); + } else { + throw new IllegalArgumentException("Unsupported where-used element kind: " + kind); // NOI18N + } + return query; + } + + private ResolvedElement resolve() throws IOException { + JavaSource source = JavaSource.forFileObject(handle.getFileObject()); + if (source == null) { + return null; + } + final ResolvedElement[] resolved = new ResolvedElement[1]; + source.runUserActionTask(new Task() { + @Override + public void run(CompilationController controller) throws Exception { + controller.toPhase(JavaSource.Phase.ELEMENTS_RESOLVED); + Element element = handle.resolveElement(controller); + if (element == null) { + return; + } + ElementKind kind = element.getKind(); + if (kind == ElementKind.CONSTRUCTOR || !(kind == ElementKind.METHOD || SUPPORTED_TYPE_KINDS.contains(kind))) { + return; + } + String displayName = kind == ElementKind.METHOD + ? element.getEnclosingElement().getSimpleName() + "." + element.getSimpleName() // NOI18N + : element.getSimpleName().toString(); + resolved[0] = new ResolvedElement(kind, displayName); + } + }, true); + return resolved[0]; + } + + private static boolean isFatal(Problem problem) { + return problem != null && problem.isFatal(); + } + + private static void checkCancelled(AtomicBoolean cancel) throws InterruptedIOException { + if (cancel != null && cancel.get()) { + throw new InterruptedIOException("Cancelled"); + } + } + } + + private static final class ReferencePresentation { + + private static final Comparator REFERENCE_ORDER = Comparator + .comparing(ReferencePresentation::sortPath) + .thenComparingInt(ReferencePresentation::sortLine) + .thenComparing(RefactoringElement::getText, Comparator.nullsLast(Comparator.naturalOrder())); + + private ReferencePresentation() { + } + + static String displayText(RefactoringElement element) { + String text = element.getDisplayText(); + if (text == null || text.isEmpty()) { + text = element.getText(); + } + return stripHtmlEnvelope(text); + } + + static String locationText(RefactoringElement element) { + FileObject file = element.getParentFile(); + if (file == null) { + return null; + } + try { + PositionBounds bounds = element.getPosition(); + if (bounds == null) { + return file.getNameExt(); + } + return file.getNameExt() + ':' + (bounds.getBegin().getLine() + 1); + } catch (IOException ex) { + return file.getNameExt(); + } + } + + static String tooltipText(RefactoringElement element) { + FileObject file = element.getParentFile(); + if (file == null) { + return displayText(element); + } + String location = locationText(element); + return location != null ? file.getPath() + " - " + location : file.getPath(); // NOI18N + } + + private static String sortPath(RefactoringElement element) { + FileObject file = element.getParentFile(); + return file != null ? file.getPath() : ""; // NOI18N + } + + private static int sortLine(RefactoringElement element) { + try { + PositionBounds bounds = element.getPosition(); + return bounds != null ? bounds.getBegin().getLine() : Integer.MAX_VALUE; + } catch (IOException ex) { + return Integer.MAX_VALUE; + } + } + + private static String stripHtmlEnvelope(String text) { + if (text == null) { + return ""; // NOI18N + } + String result = text; + if (result.regionMatches(true, 0, "", 0, 6)) { // NOI18N + result = result.substring(6); + } + if (result.regionMatches(true, Math.max(0, result.length() - 7), "", 0, 7)) { // NOI18N + result = result.substring(0, result.length() - 7); + } + return result; + } + } + + private record ResolvedElement(ElementKind kind, String displayName) { + } +} From a058c1078cc585e97cd5feee3aefb7b1325c702e Mon Sep 17 00:00:00 2001 From: Harsha Indunil Date: Sun, 29 Mar 2026 15:32:23 +0530 Subject: [PATCH 3/4] Group reference popup usages by type --- .../editor/semantic/ReferenceUsagesPopup.java | 186 +++++++++++++++++- 1 file changed, 180 insertions(+), 6 deletions(-) diff --git a/java/java.editor/src/org/netbeans/modules/java/editor/semantic/ReferenceUsagesPopup.java b/java/java.editor/src/org/netbeans/modules/java/editor/semantic/ReferenceUsagesPopup.java index 6baa10719274..b40aaa466eb6 100644 --- a/java/java.editor/src/org/netbeans/modules/java/editor/semantic/ReferenceUsagesPopup.java +++ b/java/java.editor/src/org/netbeans/modules/java/editor/semantic/ReferenceUsagesPopup.java @@ -18,6 +18,7 @@ */ package org.netbeans.modules.java.editor.semantic; +import com.sun.source.tree.Tree; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; @@ -46,6 +47,8 @@ import javax.lang.model.element.ElementKind; import javax.swing.DefaultListCellRenderer; import javax.swing.DefaultListModel; +import javax.swing.Icon; +import javax.swing.ImageIcon; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; @@ -58,6 +61,7 @@ import org.netbeans.api.java.source.JavaSource; import org.netbeans.api.java.source.Task; import org.netbeans.api.java.source.TreePathHandle; +import org.netbeans.api.java.source.ui.ElementIcons; import org.netbeans.modules.java.editor.overridden.PopupUtil; import org.netbeans.modules.refactoring.api.Problem; import org.netbeans.modules.refactoring.api.RefactoringElement; @@ -69,6 +73,7 @@ import org.netbeans.spi.editor.completion.support.CompletionUtilities; import org.openide.filesystems.FileObject; import org.openide.text.PositionBounds; +import org.openide.util.ImageUtilities; import org.openide.util.NbBundle; import org.openide.util.RequestProcessor; import org.openide.util.lookup.Lookups; @@ -214,7 +219,9 @@ private static Point popupLocation(JTextComponent component, Rectangle2D hintBou return location; } - private sealed interface PopupEntry permits ReferenceEntry, ActionEntry, MessageEntry { + private sealed interface PopupEntry permits GroupEntry, ReferenceEntry, ActionEntry, MessageEntry { + + Icon icon(); String leftHtml(); @@ -227,16 +234,53 @@ private sealed interface PopupEntry permits ReferenceEntry, ActionEntry, Message void activate(); } + private record GroupEntry(ReferenceGroup group) implements PopupEntry { + + @Override + public Icon icon() { + return group.icon(); + } + + @Override + public String leftHtml() { + return ReferencePresentation.groupText(group); + } + + @Override + public String rightText() { + return null; + } + + @Override + public String tooltip() { + return group.tooltip(); + } + + @Override + public boolean selectable() { + return false; + } + + @Override + public void activate() { + } + } + private record ReferenceEntry(RefactoringElement element) implements PopupEntry { + @Override + public Icon icon() { + return null; + } + @Override public String leftHtml() { - return ReferencePresentation.displayText(element); + return ReferencePresentation.usageText(element); } @Override public String rightText() { - return ReferencePresentation.locationText(element); + return null; } @Override @@ -257,6 +301,11 @@ public void activate() { private record ActionEntry(String text, Runnable action) implements PopupEntry { + @Override + public Icon icon() { + return null; + } + @Override public String leftHtml() { return text; @@ -285,6 +334,11 @@ public void activate() { private record MessageEntry(String text) implements PopupEntry { + @Override + public Icon icon() { + return null; + } + @Override public String leftHtml() { return text; @@ -337,7 +391,7 @@ protected void paintComponent(Graphics g) { g.setColor(bgColor); g.fillRect(0, 0, getWidth(), getHeight()); - CompletionUtilities.renderHtml(null, entry.leftHtml(), entry.rightText(), g, getFont(), fgColor, getWidth(), getHeight(), false); + CompletionUtilities.renderHtml(asImageIcon(entry.icon()), entry.leftHtml(), entry.rightText(), g, getFont(), fgColor, getWidth(), getHeight(), false); } private Color foregroundForEntry(PopupEntry entry, Color defaultColor, Color background) { @@ -403,6 +457,16 @@ private static double linearize(double component) { ? component / 12.92d : Math.pow((component + 0.055d) / 1.055d, 2.4d); } + + private static ImageIcon asImageIcon(Icon icon) { + if (icon == null) { + return null; + } + if (icon instanceof ImageIcon imageIcon) { + return imageIcon; + } + return ImageUtilities.icon2ImageIcon(icon); + } } private static final class ReferenceLoader { @@ -440,8 +504,14 @@ private List buildEntries(List references) { if (references.isEmpty()) { return List.of(new MessageEntry(Bundle.LBL_ReferencePopup_NoReferences())); } - List entries = new ArrayList<>(references.size() + 1); + List entries = new ArrayList<>(references.size() * 2 + 1); + ReferenceGroup currentGroup = null; for (RefactoringElement reference : references) { + ReferenceGroup group = ReferencePresentation.groupOf(reference); + if (currentGroup == null || !group.id().equals(currentGroup.id())) { + entries.add(new GroupEntry(group)); + currentGroup = group; + } entries.add(new ReferenceEntry(reference)); } entries.add(new ActionEntry(Bundle.LBL_ReferencePopup_ShowAll(), this::openAllReferences)); @@ -545,6 +615,14 @@ private static void checkCancelled(AtomicBoolean cancel) throws InterruptedIOExc private static final class ReferencePresentation { + private static final Icon DEFAULT_GROUP_ICON = ElementIcons.getElementIcon(ElementKind.CLASS, null); + private static final Set TYPE_TREE_KINDS = EnumSet.of( + Tree.Kind.CLASS, + Tree.Kind.INTERFACE, + Tree.Kind.ENUM, + Tree.Kind.ANNOTATION_TYPE, + Tree.Kind.RECORD); + private static final String USAGE_INDENT = "   "; // NOI18N private static final Comparator REFERENCE_ORDER = Comparator .comparing(ReferencePresentation::sortPath) .thenComparingInt(ReferencePresentation::sortLine) @@ -558,7 +636,36 @@ static String displayText(RefactoringElement element) { if (text == null || text.isEmpty()) { text = element.getText(); } - return stripHtmlEnvelope(text); + return stripColorMarkup(stripHtmlEnvelope(text)); + } + + static String usageText(RefactoringElement element) { + return USAGE_INDENT + displayText(element); + } + + static String groupText(ReferenceGroup group) { + return "" + escapeHtml(group.displayName()) + ""; // NOI18N + } + + static ReferenceGroup groupOf(RefactoringElement element) { + FileObject file = element.getParentFile(); + Object typeGrip = enclosingType(findGrip(element)); + if (typeGrip != null) { + FileObject typeFile = fileObjectOf(typeGrip); + if (typeFile == null) { + typeFile = file; + } + String filePath = typeFile != null ? typeFile.getPath() : ""; // NOI18N + String id = filePath + '#' + String.valueOf(invoke(typeGrip, "getHandle")); // NOI18N + String displayName = String.valueOf(typeGrip); + String tooltip = filePath.isEmpty() ? displayName : filePath; + Icon icon = iconOf(typeGrip); + return new ReferenceGroup(id, displayName, tooltip, icon); + } + String displayName = file != null ? file.getName() : ""; // NOI18N + String tooltip = file != null ? file.getPath() : displayName; + String id = file != null ? file.getPath() : displayName; + return new ReferenceGroup(id, displayName, tooltip, DEFAULT_GROUP_ICON); } static String locationText(RefactoringElement element) { @@ -600,6 +707,70 @@ private static int sortLine(RefactoringElement element) { } } + private static Object enclosingType(Object grip) { + Object current = grip; + while (current != null && !TYPE_TREE_KINDS.contains(kindOf(current))) { + current = invoke(current, "getParent"); + } + return current; + } + + private static Object findGrip(RefactoringElement element) { + for (Object candidate : element.getLookup().lookupAll(Object.class)) { + if (candidate != null && kindOf(candidate) != null) { + return candidate; + } + } + return null; + } + + private static Tree.Kind kindOf(Object candidate) { + Object kind = invoke(candidate, "getKind"); + return kind instanceof Tree.Kind ? (Tree.Kind) kind : null; + } + + private static FileObject fileObjectOf(Object candidate) { + Object fileObject = invoke(candidate, "getFileObject"); + return fileObject instanceof FileObject ? (FileObject) fileObject : null; + } + + private static Icon iconOf(Object candidate) { + Object icon = invoke(candidate, "getIcon"); + return icon instanceof Icon ? (Icon) icon : DEFAULT_GROUP_ICON; + } + + private static Object invoke(Object target, String methodName) { + if (target == null) { + return null; + } + try { + return target.getClass().getMethod(methodName).invoke(target); + } catch (ReflectiveOperationException | SecurityException ex) { + return null; + } + } + + private static String escapeHtml(String text) { + if (text == null || text.isEmpty()) { + return ""; // NOI18N + } + return text + .replace("&", "&") // NOI18N + .replace("<", "<") // NOI18N + .replace(">", ">"); // NOI18N + } + + private static String stripColorMarkup(String text) { + if (text == null || text.isEmpty()) { + return ""; // NOI18N + } + return text + .replaceAll("(?i)]*>", "") // NOI18N + .replaceAll("(?i)\\scolor\\s*=\\s*(['\"]).*?\\1", "") // NOI18N + .replaceAll("(?i)\\scolor\\s*=\\s*#[0-9a-f]{3,8}", "") // NOI18N + .replaceAll("(?i)\\sstyle\\s*=\\s*(['\"]).*?color\\s*:[^;'\"]*;?.*?\\1", ""); // NOI18N + } + private static String stripHtmlEnvelope(String text) { if (text == null) { return ""; // NOI18N @@ -615,6 +786,9 @@ private static String stripHtmlEnvelope(String text) { } } + private record ReferenceGroup(String id, String displayName, String tooltip, Icon icon) { + } + private record ResolvedElement(ElementKind kind, String displayName) { } } From 9b1a745d083d484841329c9125ed65b3fa484532 Mon Sep 17 00:00:00 2001 From: Harsha Indunil Date: Thu, 2 Apr 2026 03:50:37 +0530 Subject: [PATCH 4/4] Limit reference count hints to visible declarations --- .../semantic/ReferenceCountHintsTask.java | 188 +++++++++++++++--- 1 file changed, 162 insertions(+), 26 deletions(-) diff --git a/java/java.editor/src/org/netbeans/modules/java/editor/semantic/ReferenceCountHintsTask.java b/java/java.editor/src/org/netbeans/modules/java/editor/semantic/ReferenceCountHintsTask.java index 9f4e84f3bb9e..da9fda9a2218 100644 --- a/java/java.editor/src/org/netbeans/modules/java/editor/semantic/ReferenceCountHintsTask.java +++ b/java/java.editor/src/org/netbeans/modules/java/editor/semantic/ReferenceCountHintsTask.java @@ -26,13 +26,18 @@ import java.io.IOException; import java.awt.Font; import java.awt.FontMetrics; +import java.awt.Point; +import java.awt.Rectangle; import java.awt.Shape; +import java.awt.event.HierarchyEvent; +import java.awt.event.HierarchyListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -45,6 +50,9 @@ import javax.lang.model.element.ElementKind; import javax.swing.JEditorPane; import javax.swing.SwingUtilities; +import javax.swing.JViewport; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; import javax.swing.text.AttributeSet; import javax.swing.text.BadLocationException; import javax.swing.text.Document; @@ -88,8 +96,10 @@ public final class ReferenceCountHintsTask extends JavaParserResultTask DECLARATION_PARENTS = EnumSet.of( Tree.Kind.CLASS, @@ -150,12 +160,19 @@ public static OffsetsBag getBag(Document doc) { } public static void install(JEditorPane pane) { + getManager(pane.getDocument()).attachPane(pane); if (pane.getClientProperty(KEY_INTERACTION) == null) { HintInteraction interaction = new HintInteraction(pane); pane.putClientProperty(KEY_INTERACTION, interaction); pane.addMouseListener(interaction); pane.addMouseMotionListener(interaction); } + if (pane.getClientProperty(KEY_VIEWPORT_TRACKER) == null) { + ViewportTracker tracker = new ViewportTracker(pane); + pane.putClientProperty(KEY_VIEWPORT_TRACKER, tracker); + pane.addHierarchyListener(tracker); + tracker.attach(); + } } private static Manager getManager(Document doc) { @@ -178,7 +195,12 @@ public Void visitMethod(MethodTree tree, Void p) { && InlineHintsSettings.isReferenceCountMethodsEnabled()) { int[] nameSpan = info.getTreeUtilities().findNameSpan(tree); if (nameSpan != null) { - declarations.add(createDeclaration(doc, info, getCurrentPath(), nameSpan[0], element.getKind())); + declarations.add(createDeclaration( + doc, + info, + getCurrentPath(), + declarationAnchorOffset(info, tree, nameSpan[0]), + element.getKind())); } } return super.visitMethod(tree, p); @@ -196,7 +218,12 @@ && isSupportedType(element.getKind()) && DECLARATION_PARENTS.contains(parentPath.getLeaf().getKind())) { int[] nameSpan = info.getTreeUtilities().findNameSpan(tree); if (nameSpan != null) { - declarations.add(createDeclaration(doc, info, getCurrentPath(), nameSpan[0], element.getKind())); + declarations.add(createDeclaration( + doc, + info, + getCurrentPath(), + declarationAnchorOffset(info, tree, nameSpan[0]), + element.getKind())); } } return super.visitClass(tree, p); @@ -205,6 +232,14 @@ && isSupportedType(element.getKind()) return declarations; } + private static int declarationAnchorOffset(CompilationInfo info, Tree tree, int fallbackOffset) { + long start = info.getTrees().getSourcePositions().getStartPosition(info.getCompilationUnit(), tree); + if (start >= 0 && start <= Integer.MAX_VALUE) { + return (int) start; + } + return fallbackOffset; + } + private static boolean isSupportedType(ElementKind kind) { return kind == ElementKind.CLASS || kind == ElementKind.INTERFACE @@ -291,6 +326,7 @@ private static final class Manager implements ClassIndexListener, Runnable { private final Document doc; private final RequestProcessor.Task task; private final Map cache = new ConcurrentHashMap<>(); + private final Set dirtyHandles = ConcurrentHashMap.newKeySet(); private volatile List declarations = Collections.emptyList(); private volatile List hintActions = Collections.emptyList(); private volatile FileObject file; @@ -299,12 +335,19 @@ private static final class Manager implements ClassIndexListener, Runnable { private volatile ClassIndex classIndex; private volatile AtomicBoolean activeCancel = new AtomicBoolean(); private volatile Future scanRetry; + private volatile JEditorPane pane; Manager(Document doc) { this.doc = doc; this.task = WORKER.create(this); } + void attachPane(JEditorPane pane) { + if (pane != null && pane.getDocument() == doc) { + this.pane = pane; + } + } + void update(CompilationInfo info, List declarations) { FileObject file = info.getFileObject(); if (file == null) { @@ -313,11 +356,12 @@ void update(CompilationInfo info, List declarations) { } registerIndexListener(info.getClasspathInfo().getClassIndex()); Scope newScope = createScope(file); - boolean clearCache = !sameDeclarations(this.declarations, declarations) || !sameScope(this.scope, newScope); + boolean scopeChanged = !sameScope(this.scope, newScope); + updateCachedHandles(this.declarations, declarations, scopeChanged); this.file = file; this.scope = newScope; this.declarations = declarations; - schedule(clearCache); + schedule(false); } void defer(CompilationInfo info) { @@ -346,6 +390,7 @@ void clear() { scanRetry = null; } cache.clear(); + dirtyHandles.clear(); declarations = Collections.emptyList(); hintActions = Collections.emptyList(); publish(Collections.emptyList(), new OffsetsBag(doc)); @@ -357,7 +402,7 @@ private void registerIndexListener(ClassIndex newClassIndex) { } classIndex = newClassIndex; newClassIndex.addClassIndexListener(WeakListeners.create(ClassIndexListener.class, this, newClassIndex)); - cache.clear(); + dirtyHandles.addAll(handlesOf(declarations)); } private Scope createScope(FileObject file) { @@ -368,18 +413,6 @@ private Scope createScope(FileObject file) { return Scope.create(null, null, List.of(file)); } - private boolean sameDeclarations(List previous, List current) { - if (previous.size() != current.size()) { - return false; - } - for (int i = 0; i < previous.size(); i++) { - if (!previous.get(i).handle.equals(current.get(i).handle)) { - return false; - } - } - return true; - } - private boolean sameScope(Scope previous, Scope current) { return previous != null && current != null @@ -388,9 +421,29 @@ private boolean sameScope(Scope previous, Scope current) { && previous.isDependencies() == current.isDependencies(); } + private void updateCachedHandles(List previous, List current, boolean scopeChanged) { + if (scopeChanged) { + cache.clear(); + dirtyHandles.clear(); + return; + } + Set currentHandles = handlesOf(current); + cache.keySet().removeIf(handle -> !currentHandles.contains(handle)); + dirtyHandles.removeIf(handle -> !currentHandles.contains(handle)); + } + + private Set handlesOf(List declarations) { + Set handles = new HashSet<>(Math.max(4, declarations.size())); + for (Declaration declaration : declarations) { + handles.add(declaration.handle); + } + return handles; + } + private void schedule(boolean clearCache) { if (clearCache) { cache.clear(); + dirtyHandles.clear(); } serial++; activeCancel.set(true); @@ -441,16 +494,21 @@ public void run() { return; } AtomicBoolean currentCancel = activeCancel; + List visibleDeclarations = visibleDeclarations(currentDeclarations); + if (visibleDeclarations.isEmpty()) { + publish(Collections.emptyList(), new OffsetsBag(doc)); + return; + } OffsetsBag newBag = new OffsetsBag(doc); List actions = new ArrayList<>(); List debugCounts = new ArrayList<>(); - for (Declaration declaration : currentDeclarations) { + for (Declaration declaration : visibleDeclarations) { if (currentCancel.get() || currentSerial != serial) { return; } Integer cachedCount = cache.get(declaration.handle); int count; - if (cachedCount != null) { + if (cachedCount != null && !dirtyHandles.contains(declaration.handle)) { count = cachedCount; } else { boolean cacheCount = true; @@ -469,6 +527,7 @@ public void run() { } if (cacheCount) { cache.put(declaration.handle, count); + dirtyHandles.remove(declaration.handle); } } if (count <= 0) { @@ -494,12 +553,49 @@ public void run() { } } if (currentSerial == serial) { - LOG.log(Level.INFO, "Reference count hints processed for {0}: declarations={1}, published={2}, counts={3}", - new Object[]{file, currentDeclarations.size(), actions.size(), debugCounts}); + LOG.log(Level.FINE, "Reference count hints processed for {0}: visibleDeclarations={1}, published={2}, counts={3}", + new Object[]{file, visibleDeclarations.size(), actions.size(), debugCounts}); publish(actions, newBag); } } + private List visibleDeclarations(List currentDeclarations) { + JEditorPane currentPane = pane; + if (currentPane == null || currentPane.getDocument() != doc || !currentPane.isShowing()) { + return currentDeclarations.subList(0, Math.min(currentDeclarations.size(), 24)); + } + Rectangle visibleRect = currentPane.getVisibleRect(); + if (visibleRect.isEmpty()) { + return currentDeclarations.subList(0, Math.min(currentDeclarations.size(), 24)); + } + int margin = Math.max(visibleRect.height, 1) * VIEWPORT_MARGIN_MULTIPLIER; + int startOffset = viewOffset(currentPane, visibleRect.y - margin); + int endOffset = viewOffset(currentPane, visibleRect.y + visibleRect.height + margin); + if (startOffset < 0 || endOffset < 0) { + return currentDeclarations.subList(0, Math.min(currentDeclarations.size(), 24)); + } + if (startOffset > endOffset) { + int tmp = startOffset; + startOffset = endOffset; + endOffset = tmp; + } + List visible = new ArrayList<>(); + for (Declaration declaration : currentDeclarations) { + int paragraphOffset = declaration.paragraphPosition.getOffset(); + if (paragraphOffset >= startOffset && paragraphOffset <= endOffset) { + visible.add(declaration); + } + } + return visible; + } + + private int viewOffset(JEditorPane pane, int y) { + int clampedY = Math.max(0, y); + Rectangle visibleRect = pane.getVisibleRect(); + int x = Math.max(0, visibleRect.x); + return pane.viewToModel2D(new Point(x, clampedY)); + } + private void publish(List actions, OffsetsBag newBag) { SwingUtilities.invokeLater(() -> { hintActions = actions; @@ -518,27 +614,67 @@ private HintActionData findAction(int paragraphOffset) { @Override public void typesAdded(org.netbeans.api.java.source.TypesEvent event) { - schedule(true); + dirtyHandles.addAll(handlesOf(declarations)); + schedule(false); } @Override public void typesRemoved(org.netbeans.api.java.source.TypesEvent event) { - schedule(true); + dirtyHandles.addAll(handlesOf(declarations)); + schedule(false); } @Override public void typesChanged(org.netbeans.api.java.source.TypesEvent event) { - schedule(true); + dirtyHandles.addAll(handlesOf(declarations)); + schedule(false); } @Override public void rootsAdded(org.netbeans.api.java.source.RootsEvent event) { - schedule(true); + dirtyHandles.addAll(handlesOf(declarations)); + schedule(false); } @Override public void rootsRemoved(org.netbeans.api.java.source.RootsEvent event) { - schedule(true); + dirtyHandles.addAll(handlesOf(declarations)); + schedule(false); + } + } + + private static final class ViewportTracker implements HierarchyListener, ChangeListener { + + private final JEditorPane pane; + private JViewport viewport; + + ViewportTracker(JEditorPane pane) { + this.pane = pane; + } + + void attach() { + JViewport newViewport = (JViewport) SwingUtilities.getAncestorOfClass(JViewport.class, pane); + if (viewport == newViewport) { + return; + } + if (viewport != null) { + viewport.removeChangeListener(this); + } + viewport = newViewport; + if (viewport != null) { + viewport.addChangeListener(this); + } + } + + @Override + public void hierarchyChanged(HierarchyEvent e) { + attach(); + } + + @Override + public void stateChanged(ChangeEvent e) { + getManager(pane.getDocument()).attachPane(pane); + getManager(pane.getDocument()).schedule(false); } }