Merge remote-tracking branch 'origin/GP-1-dragonmcher-find-results-table-context-sorting'

This commit is contained in:
Ryan Kurtz
2026-02-17 17:11:23 -05:00
8 changed files with 301 additions and 153 deletions
@@ -15,17 +15,14 @@
*/
package ghidra.app.plugin.core.navigation.locationreferences;
import java.awt.*;
import java.awt.Component;
import java.util.*;
import javax.swing.*;
import javax.swing.text.View;
import org.apache.commons.lang3.StringUtils;
import docking.widgets.search.SearchLocationContext;
import docking.widgets.search.SearchLocationContextRenderer;
import docking.widgets.table.GTableCellRenderingData;
import generic.theme.GThemeDefaults.Colors;
import ghidra.docking.settings.Settings;
import ghidra.framework.plugintool.ServiceProvider;
import ghidra.program.model.address.Address;
@@ -33,7 +30,6 @@ import ghidra.program.model.listing.Program;
import ghidra.program.util.ProgramLocation;
import ghidra.util.datastruct.Accumulator;
import ghidra.util.exception.CancelledException;
import ghidra.util.layout.AbstractLayoutManager;
import ghidra.util.table.AddressBasedTableModel;
import ghidra.util.table.AddressPreviewTableModel;
import ghidra.util.table.column.AbstractGhidraColumnRenderer;
@@ -237,8 +233,14 @@ class LocationReferencesTableModel extends AddressBasedTableModel<LocationRefere
private class ContextCellRenderer extends AbstractGhidraColumnRenderer<LocationReference> {
private JPanel htmlContainer = new JPanel(new HtmlTruncatingLayout());
private JLabel ellipsisLabel = new JLabel("...");
private SearchLocationContextRenderer contextRenderer =
new SearchLocationContextRenderer() {
@Override
protected SearchLocationContext getContext(GTableCellRenderingData d) {
LocationReference lr = (LocationReference) d.getRowObject();
return lr.getContext();
}
};
ContextCellRenderer() {
setHTMLRenderingEnabled(true);
@@ -247,9 +249,6 @@ class LocationReferencesTableModel extends AddressBasedTableModel<LocationRefere
@Override
public Component getTableCellRendererComponent(GTableCellRenderingData data) {
// initialize
super.getTableCellRendererComponent(data);
LocationReference rowObject = (LocationReference) data.getRowObject();
Callback offcutCallback = () -> {
boolean isSelected = data.isSelected();
@@ -257,29 +256,13 @@ class LocationReferencesTableModel extends AddressBasedTableModel<LocationRefere
};
String refTypeString = getRefTypeString(rowObject, offcutCallback);
if (refTypeString != null) {
super.getTableCellRendererComponent(data);
setText(refTypeString);
return this;
}
/*
At this point we have html context. Build a renderer that is a panel with 2
children: the html label (this renderer object) and an ellipsis label that will
be visible as needed.
*/
SearchLocationContext context = rowObject.getContext();
String html = context.getBoldMatchingText();
setText(html);
ellipsisLabel.setOpaque(true);
ellipsisLabel.setForeground(Colors.FOREGROUND);
ellipsisLabel.setBackground(getBackground());
htmlContainer.setBackground(getBackground());
htmlContainer.removeAll();
htmlContainer.add(this);
htmlContainer.add(ellipsisLabel);
return htmlContainer;
return contextRenderer.renderHtmlContext(data, context);
}
@Override
@@ -294,67 +277,6 @@ class LocationReferencesTableModel extends AddressBasedTableModel<LocationRefere
}
}
/**
* A layout manager that positions 2 labels: a leading label with html and a trailing label
* with an ellipsis, which may not be visible. JLabels rendering html will not show an
* ellipsis when clipped. We use these 2 labels here to show when the leading html label's
* text is clipped.
*/
private class HtmlTruncatingLayout extends AbstractLayoutManager {
@Override
public Dimension preferredLayoutSize(Container parent) {
Dimension d = new Dimension();
int n = parent.getComponentCount();
for (int i = 0; i < n; i++) {
Component c = parent.getComponent(i);
Dimension cd = c.getPreferredSize();
d.width += cd.width;
d.height = Math.max(d.height, cd.height);
}
Insets insets = parent.getInsets();
d.width += insets.left + insets.right;
d.height += insets.top + insets.bottom;
return d;
}
@Override
public void layoutContainer(Container parent) {
// Assumption: the leading component is an html view; the trailing component is a
// label with an ellipsis
JComponent c1 = (JComponent) parent.getComponent(0);
Dimension d = parent.getSize();
Insets insets = parent.getInsets();
int width = d.width - insets.left - insets.right;
View v = (View) c1.getClientProperty("html");
Insets i = c1.getInsets();
int availableWidth = width - (i.left + i.right);
int htmlw = (int) v.getPreferredSpan(View.X_AXIS);
JLabel c2 = (JLabel) parent.getComponent(1);
Dimension c2d = c2.getPreferredSize();
boolean isClipped = htmlw > availableWidth && width != 0;
if (isClipped) {
availableWidth -= c2d.width; // save room for ellipsis
int c2x = availableWidth;
int c2y = insets.top;
c2.setBounds(c2x, c2y, c2d.width, c2d.height);
}
c2.setVisible(isClipped);
int c1x = insets.left;
int c1y = insets.top;
int cyh = d.height - (i.top + i.bottom);
c1.setBounds(c1x, c1y, availableWidth, cyh);
}
}
}
}
@@ -244,14 +244,14 @@ public class DecompilerSearcher implements FindDialogSearcher {
FieldLocation fieldLocation =
new FieldLocation(i, lineInfo.fieldNumber(), lineInfo.row(), lineInfo.column());
int lineNumber = lineInfo.lineNumber();
SearchLocationContext context = createContext(fullLine, match);
SearchLocationContext context = createContext(fullLine, lineNumber, match);
return new DecompilerSearchLocation(fieldLocation, match.start, match.end - 1,
searchString, true, field.getText(), lineNumber, context);
}
return null;
}
private SearchLocationContext createContext(String line, SearchMatch match) {
private SearchLocationContext createContext(String line, int lineNumber, SearchMatch match) {
SearchLocationContextBuilder builder = new SearchLocationContextBuilder();
int start = match.start;
int end = match.end;
@@ -261,6 +261,7 @@ public class DecompilerSearcher implements FindDialogSearcher {
builder.append(line.substring(end));
}
builder.lineNumber(lineNumber);
return builder.build();
}
@@ -246,15 +246,16 @@ public class DecompilerTextFinder {
TextLine firstLine = lineMatches.get(0);
int lineNumber = firstLine.getLineNumber();
AddressSet addresses = getAddresses(function, firstLine.getCLine());
SearchLocationContext context = createMatchContext(lineMatches);
SearchLocationContext context = createMatchContext(lineMatches, lineNumber);
TextMatch match =
new TextMatch(function, addresses, lineNumber, searchText, context, true);
callback.accept(match);
}
private SearchLocationContext createMatchContext(List<TextLine> matches) {
private SearchLocationContext createMatchContext(List<TextLine> matches, int lineNumber) {
SearchLocationContextBuilder builder = new SearchLocationContextBuilder();
builder.lineNumber(lineNumber);
for (TextLine line : matches) {
if (!builder.isEmpty()) {
builder.newline();
@@ -280,6 +281,7 @@ public class DecompilerTextFinder {
}
SearchLocationContextBuilder builder = new SearchLocationContextBuilder();
builder.lineNumber(line.getLineNumber());
int start = matcher.start();
int end = matcher.end();
@@ -21,6 +21,7 @@ import java.util.function.Consumer;
import java.util.regex.Pattern;
import docking.widgets.search.SearchLocationContext;
import docking.widgets.search.SearchLocationContextRenderer;
import docking.widgets.table.*;
import ghidra.docking.settings.Settings;
import ghidra.framework.plugintool.ServiceProvider;
@@ -30,7 +31,6 @@ import ghidra.program.model.listing.*;
import ghidra.util.datastruct.Accumulator;
import ghidra.util.exception.CancelledException;
import ghidra.util.table.GhidraProgramTableModel;
import ghidra.util.table.column.AbstractGhidraColumnRenderer;
import ghidra.util.table.column.GColumnRenderer;
import ghidra.util.table.field.AbstractProgramBasedDynamicTableColumn;
import ghidra.util.table.field.FunctionNameTableColumn;
@@ -164,37 +164,27 @@ public class DecompilerTextFinderTableModel extends GhidraProgramTableModel<Text
}
private class ContextCellRenderer
extends AbstractGhidraColumnRenderer<SearchLocationContext> {
extends SearchLocationContextRenderer {
{
// the context uses html
setHTMLRenderingEnabled(true);
@Override
protected SearchLocationContext getContext(GTableCellRenderingData d) {
TextMatch m = (TextMatch) d.getRowObject();
return m.getContext();
}
@Override
public Component getTableCellRendererComponent(GTableCellRenderingData data) {
// initialize
super.getTableCellRendererComponent(data);
TextMatch match = (TextMatch) data.getRowObject();
SearchLocationContext context = match.getContext();
String text;
if (match.isMultiLine()) {
// multi-line matches create visual noise when showing colors, as of much of the
// entire line matches
text = context.getPlainText();
return renderPlainContext(data, context);
}
else {
text = context.getBoldMatchingText();
}
setText(text);
return this;
return renderHtmlContext(data, context);
}
@Override
public String getFilterString(SearchLocationContext context, Settings settings) {
return context.getPlainText();
}
}
}
@@ -16,7 +16,6 @@
package docking.widgets;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
@@ -30,14 +29,12 @@ import javax.swing.table.TableModel;
import docking.ComponentProvider;
import docking.Tool;
import docking.action.DockingAction;
import docking.widgets.search.SearchLocationContext;
import docking.widgets.search.SearchResults;
import docking.widgets.search.*;
import docking.widgets.table.*;
import docking.widgets.table.actions.DeleteTableRowAction;
import ghidra.docking.settings.Settings;
import ghidra.framework.plugintool.ServiceProvider;
import ghidra.framework.plugintool.ServiceProviderStub;
import ghidra.util.table.column.AbstractGColumnRenderer;
import ghidra.util.table.column.GColumnRenderer;
public class FindDialogResultsProvider extends ComponentProvider
@@ -290,13 +287,17 @@ public class FindDialogResultsProvider extends ComponentProvider
private class ContextColumn extends
AbstractDynamicTableColumnStub<SearchLocation, SearchLocationContext> {
private GColumnRenderer<SearchLocationContext> renderer = new ContextCellRenderer();
private SearchLocationContextRenderer renderer = new SearchLocationContextRenderer() {
@Override
protected SearchLocationContext getContext(GTableCellRenderingData d) {
SearchLocation s = (SearchLocation) d.getRowObject();
return s.getContext();
}
};
@Override
public SearchLocationContext getValue(SearchLocation rowObject,
Settings settings,
ServiceProvider sp) throws IllegalArgumentException {
Settings settings, ServiceProvider sp) throws IllegalArgumentException {
SearchLocationContext context = rowObject.getContext();
return context;
}
@@ -310,33 +311,6 @@ public class FindDialogResultsProvider extends ComponentProvider
public GColumnRenderer<SearchLocationContext> getColumnRenderer() {
return renderer;
}
private class ContextCellRenderer
extends AbstractGColumnRenderer<SearchLocationContext> {
{
// the context uses html
setHTMLRenderingEnabled(true);
}
@Override
public Component getTableCellRendererComponent(GTableCellRenderingData cellData) {
// initialize
super.getTableCellRendererComponent(cellData);
SearchLocation match = (SearchLocation) cellData.getRowObject();
SearchLocationContext context = match.getContext();
String text = context.getBoldMatchingText();
setText(text);
return this;
}
@Override
public String getFilterString(SearchLocationContext context, Settings settings) {
return context.getPlainText();
}
}
}
}
}
@@ -26,7 +26,7 @@ import ghidra.util.HTMLUtilities;
*
* @see SearchLocationContextBuilder
*/
public class SearchLocationContext {
public class SearchLocationContext implements Comparable<SearchLocationContext> {
private static final String EMBOLDEN_START =
"<span style=\"background-color: #a3e4d7; color: black;\"><b><font size=4>";
@@ -88,11 +88,27 @@ public class SearchLocationContext {
}
/**
* The full plain text of this context.
* The full plain text of this context. Any non-negative line number will be prepended to the
* text.
* @return the text
*/
public String getPlainText() {
String lnText = getLineNumberText(false);
return getPlainText(true);
}
/**
* Returns the plain text of this context, without html markup.
*
* @param includeLineNumber if true, any non-negative line number will be prepended to the text.
* @return the text
*/
public String getPlainText(boolean includeLineNumber) {
String lnText = "";
if (includeLineNumber) {
lnText = getLineNumberText(false);
}
StringBuilder buffy = new StringBuilder(lnText);
for (Part part : parts) {
buffy.append(part.getText());
@@ -124,8 +140,8 @@ public class SearchLocationContext {
}
/**
* Returns HTML text for this context. Any matching items embedded in the returned string will
* be bold.
* Returns HTML text for this context. Any matching items embedded in the returned string will
* be bold. Any non-negative line number will be prepended to the text.
* @return the text
*/
public String getBoldMatchingText() {
@@ -137,6 +153,27 @@ public class SearchLocationContext {
return HTMLUtilities.HTML + buffy.toString();
}
/**
* Returns HTML text for this context. Any matching items embedded in the returned string will
* be bold.
*
* @param includeLineNumber if true, any non-negative line number will be prepended to the text.
* @return the text
*/
public String getBoldMatchingText(boolean includeLineNumber) {
String lnText = "";
if (includeLineNumber) {
lnText = getLineNumberText(false);
}
StringBuilder buffy = new StringBuilder(lnText);
for (Part part : parts) {
buffy.append(part.getHtmlText());
}
return HTMLUtilities.HTML + buffy.toString();
}
/**
* Returns any sub-strings of this context's overall text that match client-defined input
*
@@ -166,10 +203,55 @@ public class SearchLocationContext {
return getPlainText();
}
@Override
public int compareTo(SearchLocationContext other) {
// Use line numbers when both clients have them, as string integer comparisons do not
// naturally sort by integer value.
int l1 = getLineNumber();
int l2 = other.getLineNumber();
int result = Integer.compare(l1, l2);
if (result != 0) {
return result;
}
// Note: the debug text will call out the portion of the line that matches. For
// multiple matches on the same line, we will have multiple rows. In that case,
// we need the match markup to help sort those lines.
String t1 = getDebugText();
String t2 = other.getDebugText();
return t1.compareTo(t2);
}
@Override
public int hashCode() {
return Objects.hash(lineNumber, parts);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
SearchLocationContext other = (SearchLocationContext) obj;
return lineNumber == other.lineNumber && Objects.equals(parts, other.parts);
}
//=================================================================================================
// Inner Classes
//=================================================================================================
/**
* A class that represents one or more characters within the full text of this context class
*/
static abstract class Part {
protected String text;
Part(String text) {
@@ -189,6 +271,26 @@ public class SearchLocationContext {
return updated;
}
@Override
public int hashCode() {
return Objects.hash(text);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Part other = (Part) obj;
return Objects.equals(text, other.text);
}
@Override
public String toString() {
return Json.toString(this);
@@ -0,0 +1,156 @@
/* ###
* IP: GHIDRA
*
* Licensed 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 docking.widgets.search;
import java.awt.*;
import javax.swing.*;
import javax.swing.text.View;
import docking.widgets.table.GTableCellRenderingData;
import generic.theme.GThemeDefaults.Colors;
import ghidra.docking.settings.Settings;
import ghidra.util.layout.AbstractLayoutManager;
import ghidra.util.table.column.AbstractGColumnRenderer;
/**
* A renderer for {@link SearchLocationContext}. This renderer handles the complexity of rendering
* html text with clipping.
*/
public abstract class SearchLocationContextRenderer
extends AbstractGColumnRenderer<SearchLocationContext> {
private JPanel htmlContainer = new JPanel(new HtmlTruncatingLayout());
private JLabel ellipsisLabel = new JLabel("...");
public SearchLocationContextRenderer() {
setHTMLRenderingEnabled(true);
}
protected abstract SearchLocationContext getContext(GTableCellRenderingData data);
@Override
public Component getTableCellRendererComponent(GTableCellRenderingData data) {
SearchLocationContext context = getContext(data);
return renderHtmlContext(data, context);
}
public Component renderPlainContext(GTableCellRenderingData data,
SearchLocationContext context) {
super.getTableCellRendererComponent(data);
// Note: we do not include the line number prefix on the text, based on the assumption that
// clients of this renderer will have a separate line number column.
String text = context.getPlainText(false);
setText(text);
return this;
}
public Component renderHtmlContext(GTableCellRenderingData data,
SearchLocationContext context) {
// initialize
super.getTableCellRendererComponent(data);
/*
We have html context. Build a renderer that is a panel with 2 children: the html label
(this renderer object) and an ellipsis label that will be visible as needed.
*/
// Note: we do not include the line number prefix on the text, based on the assumption that
// clients of this renderer will have a separate line number column.
String html = context.getBoldMatchingText(false);
setText(html);
ellipsisLabel.setOpaque(true);
ellipsisLabel.setForeground(Colors.FOREGROUND);
ellipsisLabel.setBackground(getBackground());
htmlContainer.setBackground(getBackground());
htmlContainer.removeAll();
htmlContainer.add(this);
htmlContainer.add(ellipsisLabel);
return htmlContainer;
}
@Override
public String getFilterString(SearchLocationContext rowObject, Settings settings) {
return rowObject.getPlainText();
}
/**
* A layout manager that positions 2 labels: a leading label with html and a trailing label
* with an ellipsis, which may not be visible. JLabels rendering html will not show an
* ellipsis when clipped. We use these 2 labels here to show when the leading html label's
* text is clipped.
*/
private class HtmlTruncatingLayout extends AbstractLayoutManager {
@Override
public Dimension preferredLayoutSize(Container parent) {
Dimension d = new Dimension();
int n = parent.getComponentCount();
for (int i = 0; i < n; i++) {
Component c = parent.getComponent(i);
Dimension cd = c.getPreferredSize();
d.width += cd.width;
d.height = Math.max(d.height, cd.height);
}
Insets insets = parent.getInsets();
d.width += insets.left + insets.right;
d.height += insets.top + insets.bottom;
return d;
}
@Override
public void layoutContainer(Container parent) {
// Assumption: the leading component is an html view; the trailing component is a
// label with an ellipsis
JComponent c1 = (JComponent) parent.getComponent(0);
Dimension d = parent.getSize();
Insets insets = parent.getInsets();
int width = d.width - insets.left - insets.right;
View v = (View) c1.getClientProperty("html");
Insets i = c1.getInsets();
int availableWidth = width - (i.left + i.right);
int htmlw = (int) v.getPreferredSpan(View.X_AXIS);
JLabel c2 = (JLabel) parent.getComponent(1);
Dimension c2d = c2.getPreferredSize();
boolean isClipped = htmlw > availableWidth && width != 0;
if (isClipped) {
availableWidth -= c2d.width; // save room for ellipsis
int c2x = availableWidth;
int c2y = insets.top;
c2.setBounds(c2x, c2y, c2d.width, c2d.height);
}
c2.setVisible(isClipped);
int c1x = insets.left;
int c1y = insets.top;
int cyh = d.height - (i.top + i.bottom);
c1.setBounds(c1x, c1y, availableWidth, cyh);
}
}
}
@@ -270,6 +270,7 @@ public class TextComponentSearcher implements FindDialogSearcher {
private SearchLocationContext createContext(Line line, int start, int end) {
SearchLocationContextBuilder builder = new SearchLocationContextBuilder();
builder.lineNumber(line.lineNumber);
String text = line.text();
int offset = line.offset(); // document offset
int rstart = start - offset; // line-relative start