Merge remote-tracking branch

'origin/GP-3491-dragonmacher-decompiler-find-window--SQUASHED'
(Closes #5317, #538)
This commit is contained in:
Ryan Kurtz 2024-06-25 12:02:05 -04:00
commit b68fa6c745
13 changed files with 660 additions and 300 deletions

View File

@ -67,8 +67,7 @@ public class FindPossibleReferencesPlugin extends Plugin {
final static String RESTORE_SELECTION_ACTION_NAME = "Restore Direct Refs Search Selection";
final static String SEARCH_DIRECT_REFS_ACTION_NAME = "Search for Direct References";
final static String SEARCH_DIRECT_REFS_ACTION_HELP = "Direct_Refs_Search_Alignment";
private DockingAction action;
private ArrayList<TableComponentProvider<ReferenceAddressPair>> providerList;
private List<TableComponentProvider<ReferenceAddressPair>> providerList;
public FindPossibleReferencesPlugin(PluginTool tool) {
super(tool);
@ -98,22 +97,21 @@ public class FindPossibleReferencesPlugin extends Plugin {
}
private void createActions() {
action = new ActionBuilder(SEARCH_DIRECT_REFS_ACTION_NAME, getName())
.menuPath(ToolConstants.MENU_SEARCH, "For Direct References")
.menuGroup("search for", "DirectReferences")
.helpLocation(new HelpLocation(HelpTopics.SEARCH, SEARCH_DIRECT_REFS_ACTION_NAME))
.description(getPluginDescription().getDescription())
.withContext(ListingActionContext.class, true)
.inWindow(ActionBuilder.When.CONTEXT_MATCHES)
.onAction(this::findReferences)
.enabledWhen(this::hasCorrectAddressSize)
.buildAndInstall(tool);
new ActionBuilder(SEARCH_DIRECT_REFS_ACTION_NAME, getName())
.menuPath(ToolConstants.MENU_SEARCH, "For Direct References")
.menuGroup("search for", "DirectReferences")
.helpLocation(new HelpLocation(HelpTopics.SEARCH, SEARCH_DIRECT_REFS_ACTION_NAME))
.description(getPluginDescription().getDescription())
.withContext(ListingActionContext.class, true)
.inWindow(ActionBuilder.When.CONTEXT_MATCHES)
.onAction(this::findReferences)
.enabledWhen(this::hasCorrectAddressSize)
.buildAndInstall(tool);
}
private boolean hasCorrectAddressSize(NavigatableActionContext context) {
int size =
context.getProgram().getAddressFactory().getDefaultAddressSpace().getSize();
int size = context.getProgram().getAddressFactory().getDefaultAddressSpace().getSize();
if ((size == 64) || (size == 32) || (size == 24) || (size == 16) || (size == 20) ||
(size == 21)) {
return true;
@ -142,10 +140,10 @@ public class FindPossibleReferencesPlugin extends Plugin {
localAction.setHelpLocation(
new HelpLocation(HelpTopics.SEARCH, RESTORE_SELECTION_ACTION_NAME));
String group = "selection";
localAction.setMenuBarData(
new MenuData(new String[] { "Restore Search Selection" }, group));
localAction.setPopupMenuData(
new MenuData(new String[] { "Restore Search Selection" }, group));
localAction
.setMenuBarData(new MenuData(new String[] { "Restore Search Selection" }, group));
localAction
.setPopupMenuData(new MenuData(new String[] { "Restore Search Selection" }, group));
localAction.setDescription(
"Sets the program selection back to the selection this search was based upon.");
@ -194,9 +192,8 @@ public class FindPossibleReferencesPlugin extends Plugin {
return;
}
if (currentProgram.getMemory()
.getBlock(
fromAddr)
.getType() == MemoryBlockType.BIT_MAPPED) {
.getBlock(fromAddr)
.getType() == MemoryBlockType.BIT_MAPPED) {
Msg.showWarn(getClass(), null, "Search For Direct References",
"Cannot search for direct references on bit memory!");
return;

View File

@ -45,6 +45,8 @@ import ghidra.util.HelpLocation;
import ghidra.util.table.*;
import ghidra.util.table.actions.DeleteTableRowAction;
import ghidra.util.table.actions.MakeProgramSelectionAction;
import utility.function.Callback;
import utility.function.Dummy;
public class TableComponentProvider<T> extends ComponentProviderAdapter
implements TableModelListener, NavigatableRemovalListener {
@ -60,11 +62,13 @@ public class TableComponentProvider<T> extends ComponentProviderAdapter
private String programName;
private String windowSubMenu;
private List<ComponentProviderActivationListener> activationListenerList = new ArrayList<>();
private Callback closedCallback = Dummy.callback();
private Navigatable navigatable;
private SelectionNavigationAction selectionNavigationAction;
private DockingAction selectAction;
private DockingAction removeItemsAction;
private DockingAction externalGotoAction;
private Function<MouseEvent, ActionContext> contextProvider = null;
@ -170,7 +174,7 @@ public class TableComponentProvider<T> extends ComponentProviderAdapter
selectionNavigationAction
.setHelpLocation(new HelpLocation(HelpTopics.SEARCH, "Selection_Navigation"));
DockingAction externalGotoAction = new DockingAction("Go to External Location", getName()) {
externalGotoAction = new DockingAction("Go to External Location", getName()) {
@Override
public void actionPerformed(ActionContext context) {
gotoExternalAddress(getSelectedExternalAddress());
@ -203,9 +207,21 @@ public class TableComponentProvider<T> extends ComponentProviderAdapter
new MenuData(new String[] { "GoTo External Location" }, icon, null));
externalGotoAction.setHelpLocation(new HelpLocation(HelpTopics.SEARCH, "Navigation"));
plugin.getTool().addLocalAction(this, selectAction);
plugin.getTool().addLocalAction(this, selectionNavigationAction);
plugin.getTool().addLocalAction(this, externalGotoAction);
tool.addLocalAction(this, selectAction);
tool.addLocalAction(this, selectionNavigationAction);
tool.addLocalAction(this, externalGotoAction);
}
public void removeAllActions() {
tool.removeLocalAction(this, externalGotoAction);
tool.removeLocalAction(this, selectAction);
tool.removeLocalAction(this, selectionNavigationAction);
// this action is conditionally added
if (removeItemsAction != null) {
tool.removeAction(removeItemsAction);
removeItemsAction = null;
}
}
public void installRemoveItemsAction() {
@ -295,6 +311,8 @@ public class TableComponentProvider<T> extends ComponentProviderAdapter
@Override
public void closeComponent() {
this.closedCallback.call();
if (navigatable != null) {
navigatable.removeNavigatableListener(this);
}
@ -419,4 +437,12 @@ public class TableComponentProvider<T> extends ComponentProviderAdapter
public void setActionContextProvider(Function<MouseEvent, ActionContext> contextProvider) {
this.contextProvider = contextProvider;
}
/**
* Sets a listener to know when this provider is closed.
* @param c the callback
*/
public void setClosedCallback(Callback c) {
this.closedCallback = Dummy.ifNull(c);
}
}

View File

@ -19,11 +19,7 @@ import java.awt.*;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.*;
import org.apache.commons.lang3.StringUtils;
import docking.widgets.SearchLocation;
import docking.widgets.fieldpanel.Layout;
import docking.widgets.fieldpanel.LayoutModel;
import docking.widgets.fieldpanel.field.*;
@ -31,12 +27,10 @@ import docking.widgets.fieldpanel.listener.IndexMapper;
import docking.widgets.fieldpanel.listener.LayoutModelListener;
import docking.widgets.fieldpanel.support.*;
import ghidra.app.decompiler.*;
import ghidra.app.plugin.core.decompile.actions.FieldBasedSearchLocation;
import ghidra.app.util.viewer.field.CommentUtils;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.Program;
import ghidra.program.model.pcode.HighFunction;
import ghidra.util.Msg;
/**
* Control the GUI layout for displaying tokenized C code
@ -335,230 +329,11 @@ public class ClangLayoutController implements LayoutModel, LayoutModelListener {
return null;
}
//==================================================================================================
// Search Related Methods
//==================================================================================================
private SearchLocation findNextTokenGoingForward(
java.util.function.Function<String, SearchMatch> matcher, String searchString,
FieldLocation currentLocation) {
int startRow = currentLocation.getIndex().intValue();
for (int row = startRow; row < fieldList.length; row++) {
ClangTextField field = (ClangTextField) fieldList[row];
FieldLocation location = (row == startRow) ? currentLocation : null;
String lineText = getLineTextFromOffset(location, field, true);
SearchMatch match = matcher.apply(lineText);
if (match == SearchMatch.NO_MATCH) {
continue;
}
if (row == startRow) { // cursor is on this line
//
// The match start for all lines without the cursor will be relative to the start
// of the line, which is 0. However, when searching on the row with the cursor,
// the match start is relative to the cursor position. Update the start to
// compensate for the difference between the start of the line and the cursor.
//
String fullLine = field.getText();
int cursorOffset = fullLine.length() - lineText.length();
match.start += cursorOffset;
match.end += cursorOffset;
}
// we use 0 here because currently there is only one field, which is the entire line
int fieldNum = 0;
int column = getScreenColumnFromOffset(match.start, field);
FieldLocation fieldLocation = new FieldLocation(row, fieldNum, 0, column);
return new FieldBasedSearchLocation(fieldLocation, match.start, match.end - 1,
searchString, true);
}
return null;
}
private SearchLocation findNextTokenGoingBackward(
java.util.function.Function<String, SearchMatch> matcher, String searchString,
FieldLocation currentLocation) {
int startRow = currentLocation.getIndex().intValue();
for (int row = startRow; row >= 0; row--) {
ClangTextField field = (ClangTextField) fieldList[row];
FieldLocation location = (row == startRow) ? currentLocation : null;
String lineText = getLineTextFromOffset(location, field, false);
SearchMatch match = matcher.apply(lineText);
if (match != SearchMatch.NO_MATCH) {
// we use 0 here because currently there is only one field, which is the entire line
int fieldNum = 0;
int column = getScreenColumnFromOffset(match.start, field);
FieldLocation fieldLocation = new FieldLocation(row, fieldNum, 0, column);
return new FieldBasedSearchLocation(fieldLocation, match.start, match.end - 1,
searchString, false);
}
}
return null;
}
public SearchLocation findNextTokenForSearchRegex(String searchString,
FieldLocation currentLocation, boolean forwardSearch) {
Pattern pattern = null;
try {
pattern = Pattern.compile(searchString, Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
}
catch (PatternSyntaxException e) {
Msg.showError(this, decompilerPanel, "Regular Expression Syntax Error", e.getMessage());
return null;
}
Pattern finalPattern = pattern;
if (forwardSearch) {
java.util.function.Function<String, SearchMatch> function = textLine -> {
Matcher matcher = finalPattern.matcher(textLine);
if (matcher.find()) {
int start = matcher.start();
int end = matcher.end();
return new SearchMatch(start, end, textLine);
}
return SearchMatch.NO_MATCH;
};
return findNextTokenGoingForward(function, searchString, currentLocation);
}
java.util.function.Function<String, SearchMatch> reverse = textLine -> {
Matcher matcher = finalPattern.matcher(textLine);
if (!matcher.find()) {
return SearchMatch.NO_MATCH;
}
int start = matcher.start();
int end = matcher.end();
// Since the matcher can only match from the start to end of line, we need
// to find all matches and then take the last match
while (matcher.find()) {
start = matcher.start();
end = matcher.end();
}
return new SearchMatch(start, end, textLine);
};
return findNextTokenGoingBackward(reverse, searchString, currentLocation);
}
public SearchLocation findNextTokenForSearch(String searchString, FieldLocation currentLocation,
boolean forwardSearch) {
if (forwardSearch) {
java.util.function.Function<String, SearchMatch> function = textLine -> {
int index = StringUtils.indexOfIgnoreCase(textLine, searchString);
if (index == -1) {
return SearchMatch.NO_MATCH;
}
return new SearchMatch(index, index + searchString.length(), textLine);
};
return findNextTokenGoingForward(function, searchString, currentLocation);
}
java.util.function.Function<String, SearchMatch> function = textLine -> {
int index = StringUtils.lastIndexOfIgnoreCase(textLine, searchString);
if (index == -1) {
return SearchMatch.NO_MATCH;
}
return new SearchMatch(index, index + searchString.length(), textLine);
};
return findNextTokenGoingBackward(function, searchString, currentLocation);
}
private String getLineTextFromOffset(FieldLocation location, ClangTextField textField,
boolean forwardSearch) {
if (location == null) { // the cursor location is not on this line; use all of the text
return textField.getText();
}
if (textField.getText().isEmpty()) { // the cursor is on blank line
return "";
}
String lineText = textField.getText();
if (forwardSearch) {
int nextCol = location.getCol();
// protects against the location column being out of range (this can happen if we're
// searching forward and the cursor is past the last token)
if (nextCol >= lineText.length()) {
return "";
}
// skip a character to start the next search; this prevents matching the previous match
return lineText.substring(nextCol);
}
// backwards search
return lineText.substring(0, location.getCol());
}
private int getScreenColumnFromOffset(int textOffset, ClangTextField textField) {
RowColLocation rowColLocation = textField.textOffsetToScreenLocation(textOffset);
return rowColLocation.col();
}
private static class SearchMatch {
private static SearchMatch NO_MATCH = new SearchMatch(-1, -1, null);
private int start;
private int end;
private String textLine;
SearchMatch(int start, int end, String textLine) {
this.start = start;
this.end = end;
this.textLine = textLine;
}
@Override
public String toString() {
if (this == NO_MATCH) {
return "NO MATCH";
}
return "[start=" + start + ",end=" + end + "]: " + textLine;
}
}
//==================================================================================================
// End Search Related Methods
//==================================================================================================
ClangToken getTokenForLocation(FieldLocation fieldLocation) {
int row = fieldLocation.getIndex().intValue();
ClangTextField field = (ClangTextField) fieldList[row];
return field.getToken(fieldLocation);
}
public void locationChanged(FieldLocation loc, Field field, Color locationColor,
Color parenColor) {
// Highlighting is now handled through the decompiler panel's highlight controller.
}
public boolean changePending() {
return false;
}
@Override
public void flushChanges() {
// nothing to do

View File

@ -0,0 +1,219 @@
/* ###
* 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 ghidra.app.decompiler.component;
import java.util.List;
import java.util.stream.Collectors;
import javax.swing.ListSelectionModel;
import docking.DockingWindowManager;
import docking.Tool;
import docking.widgets.FindDialog;
import docking.widgets.SearchLocation;
import docking.widgets.button.GButton;
import docking.widgets.fieldpanel.support.FieldLocation;
import docking.widgets.table.AbstractDynamicTableColumnStub;
import docking.widgets.table.TableColumnDescriptor;
import ghidra.app.plugin.core.decompile.actions.DecompilerSearchLocation;
import ghidra.app.plugin.core.decompile.actions.DecompilerSearcher;
import ghidra.app.plugin.core.table.TableComponentProvider;
import ghidra.app.util.HelpTopics;
import ghidra.app.util.query.TableService;
import ghidra.docking.settings.Settings;
import ghidra.framework.plugintool.ServiceProvider;
import ghidra.program.model.listing.Program;
import ghidra.program.util.ProgramLocation;
import ghidra.program.util.ProgramSelection;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
import ghidra.util.datastruct.Accumulator;
import ghidra.util.exception.CancelledException;
import ghidra.util.table.*;
import ghidra.util.task.TaskMonitor;
public class DecompilerFindDialog extends FindDialog {
private DecompilerPanel decompilerPanel;
public DecompilerFindDialog(DecompilerPanel decompilerPanel) {
super("Decompiler Find Text", new DecompilerSearcher(decompilerPanel));
this.decompilerPanel = decompilerPanel;
setHelpLocation(new HelpLocation(HelpTopics.DECOMPILER, "ActionFind"));
GButton showAllButton = new GButton("Search All");
showAllButton.addActionListener(e -> showAll());
// move this button to the end
removeButton(dismissButton);
addButton(showAllButton);
addButton(dismissButton);
}
private void showAll() {
close();
DockingWindowManager dwm = DockingWindowManager.getActiveInstance();
Tool tool = dwm.getTool();
TableService tableService = tool.getService(TableService.class);
if (tableService == null) {
Msg.error(this,
"Cannot use the Decompiler Search All action without having a TableService " +
"installed");
return;
}
List<SearchLocation> results = searcher.searchAll(getSearchText(), useRegex());
if (!results.isEmpty()) {
// save off searches that find results so users can reuse them later
storeSearchText(getSearchText());
}
Program program = decompilerPanel.getProgram();
DecompilerFindResultsModel model = new DecompilerFindResultsModel(tool, program, results);
String title = "Decompiler Search '%s'".formatted(getSearchText());
String type = "Decompiler Search ";
String subMenuName = "Search";
TableComponentProvider<DecompilerSearchLocation> provider =
tableService.showTable(title, type, model, subMenuName, null);
// The Decompiler does not support some of the table's basic actions, such as making
// selections for a given row, so remove them.
provider.removeAllActions();
provider.installRemoveItemsAction();
GhidraThreadedTablePanel<DecompilerSearchLocation> panel = provider.getThreadedTablePanel();
GhidraTable table = panel.getTable();
// add row listener to go to the field for that row
ListSelectionModel selectionModel = table.getSelectionModel();
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
selectionModel.addListSelectionListener(lse -> {
if (lse.getValueIsAdjusting()) {
return;
}
int row = table.getSelectedRow();
if (row == -1) {
searcher.highlightSearchResults(null);
return;
}
DecompilerSearchLocation location = model.getRowObject(row);
notifySearchHit(location);
});
// add listener to table closed to clear highlights
provider.setClosedCallback(() -> decompilerPanel.setSearchResults(null));
// set the tab text to the short and descriptive search term
provider.setTabText("'%s'".formatted(getSearchText()));
}
@Override
protected void dialogClosed() {
// clear the search results when the dialog is closed
decompilerPanel.setSearchResults(null);
}
//=================================================================================================
// Inner Classes
//=================================================================================================
private class DecompilerFindResultsModel
extends GhidraProgramTableModel<DecompilerSearchLocation> {
private List<DecompilerSearchLocation> searchLocations;
DecompilerFindResultsModel(ServiceProvider sp, Program program,
List<SearchLocation> searchLocations) {
super("Decompiler Search All Results", sp, program, null);
this.searchLocations = searchLocations.stream()
.map(l -> (DecompilerSearchLocation) l)
.collect(Collectors.toList());
}
@Override
protected TableColumnDescriptor<DecompilerSearchLocation> createTableColumnDescriptor() {
TableColumnDescriptor<DecompilerSearchLocation> descriptor =
new TableColumnDescriptor<>();
descriptor.addVisibleColumn(new LineNumberColumn(), 1, true);
descriptor.addVisibleColumn(new ContextColumn());
return descriptor;
}
@Override
protected void doLoad(Accumulator<DecompilerSearchLocation> accumulator,
TaskMonitor monitor)
throws CancelledException {
for (DecompilerSearchLocation location : searchLocations) {
accumulator.add(location);
}
}
@Override
public ProgramLocation getProgramLocation(int modelRow, int modelColumn) {
return null; // This doesn't really make sense for this model
}
@Override
public ProgramSelection getProgramSelection(int[] modelRows) {
return new ProgramSelection(); // This doesn't really make sense for this model
}
private class LineNumberColumn
extends AbstractDynamicTableColumnStub<DecompilerSearchLocation, Integer> {
@Override
public Integer getValue(DecompilerSearchLocation rowObject, Settings settings,
ServiceProvider sp) throws IllegalArgumentException {
FieldLocation fieldLocation = rowObject.getFieldLocation();
return fieldLocation.getIndex().intValue() + 1; // +1 for 1-based lines
}
@Override
public String getColumnName() {
return "Line";
}
@Override
public int getColumnPreferredWidth() {
return 75;
}
}
private class ContextColumn
extends AbstractDynamicTableColumnStub<DecompilerSearchLocation, String> {
@Override
public String getValue(DecompilerSearchLocation rowObject, Settings settings,
ServiceProvider sp) throws IllegalArgumentException {
return rowObject.getTextLine();
}
@Override
public String getColumnName() {
return "Context";
}
}
}
}

View File

@ -42,7 +42,7 @@ import ghidra.app.decompiler.*;
import ghidra.app.decompiler.component.hover.DecompilerHoverService;
import ghidra.app.decompiler.component.margin.*;
import ghidra.app.plugin.core.decompile.DecompilerClipboardProvider;
import ghidra.app.plugin.core.decompile.actions.FieldBasedSearchLocation;
import ghidra.app.plugin.core.decompile.actions.DecompilerSearchLocation;
import ghidra.app.util.viewer.util.ScrollpaneAlignedHorizontalLayout;
import ghidra.program.model.address.*;
import ghidra.program.model.listing.Function;
@ -988,33 +988,15 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field
location.getIndex().intValue(), location.col);
}
//==================================================================================================
// Search Methods
//==================================================================================================
public SearchLocation searchText(String text, FieldLocation startLocation,
boolean forwardDirection) {
return layoutController.findNextTokenForSearch(text, startLocation, forwardDirection);
}
public SearchLocation searchTextRegex(String text, FieldLocation startLocation,
boolean forwardDirection) {
return layoutController.findNextTokenForSearchRegex(text, startLocation, forwardDirection);
}
public void setSearchResults(SearchLocation searchLocation) {
currentSearchLocation = searchLocation;
repaint();
}
public FieldBasedSearchLocation getSearchResults() {
return (FieldBasedSearchLocation) currentSearchLocation;
public DecompilerSearchLocation getSearchResults() {
return (DecompilerSearchLocation) currentSearchLocation;
}
//==================================================================================================
// End Search Methods
//==================================================================================================
public Color getCurrentVariableHighlightColor() {
return currentVariableHighlightColor;
}
@ -1279,7 +1261,7 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field
int highlightLine = cField.getLineNumber();
FieldLocation searchCursorLocation =
((FieldBasedSearchLocation) currentSearchLocation).getFieldLocation();
((DecompilerSearchLocation) currentSearchLocation).getFieldLocation();
int searchLineNumber = searchCursorLocation.getIndex().intValue() + 1;
if (highlightLine != searchLineNumber) {
// only highlight the match on the actual line

View File

@ -19,21 +19,27 @@ import docking.widgets.CursorPosition;
import docking.widgets.SearchLocation;
import docking.widgets.fieldpanel.support.FieldLocation;
public class FieldBasedSearchLocation extends SearchLocation {
public class DecompilerSearchLocation extends SearchLocation {
private final FieldLocation fieldLocation;
private String textLine;
public FieldBasedSearchLocation(FieldLocation fieldLocation, int startIndexInclusive,
int endIndexInclusive, String searchText, boolean forwardDirection) {
public DecompilerSearchLocation(FieldLocation fieldLocation, int startIndexInclusive,
int endIndexInclusive, String searchText, boolean forwardDirection, String textLine) {
super(startIndexInclusive, endIndexInclusive, searchText, forwardDirection);
this.fieldLocation = fieldLocation;
this.textLine = textLine;
}
public FieldLocation getFieldLocation() {
return fieldLocation;
}
public String getTextLine() {
return textLine;
}
@Override
public CursorPosition getCursorPosition() {
return new DecompilerCursorPosition(fieldLocation);

View File

@ -15,14 +15,18 @@
*/
package ghidra.app.plugin.core.decompile.actions;
import java.util.List;
import java.util.Objects;
import java.util.*;
import java.util.function.Function;
import java.util.regex.*;
import docking.widgets.*;
import docking.widgets.fieldpanel.field.Field;
import docking.widgets.fieldpanel.support.FieldLocation;
import docking.widgets.fieldpanel.support.RowColLocation;
import ghidra.app.decompiler.component.ClangTextField;
import ghidra.app.decompiler.component.DecompilerPanel;
import ghidra.util.Msg;
import ghidra.util.UserSearchUtils;
/**
* A {@link FindDialogSearcher} for searching the text of the decompiler window.
@ -86,15 +90,14 @@ public class DecompilerSearcher implements FindDialogSearcher {
DecompilerCursorPosition decompilerCursorPosition = (DecompilerCursorPosition) position;
FieldLocation startLocation =
getNextSearchStartLocation(decompilerCursorPosition, searchForward);
return useRegex ? decompilerPanel.searchTextRegex(text, startLocation, searchForward)
: decompilerPanel.searchText(text, startLocation, searchForward);
return doFind(text, startLocation, searchForward, useRegex);
}
private FieldLocation getNextSearchStartLocation(
DecompilerCursorPosition decompilerCursorPosition, boolean searchForward) {
FieldLocation startLocation = decompilerCursorPosition.getFieldLocation();
FieldBasedSearchLocation currentSearchLocation = decompilerPanel.getSearchResults();
DecompilerSearchLocation currentSearchLocation = decompilerPanel.getSearchResults();
if (currentSearchLocation == null) {
return startLocation; // nothing to do; no prior search hit
}
@ -139,4 +142,229 @@ public class DecompilerSearcher implements FindDialogSearcher {
return startLocation;
}
//=================================================================================================
// Search Methods
//=================================================================================================
@Override
public List<SearchLocation> searchAll(String searchString, boolean isRegex) {
Pattern pattern = createPattern(searchString, isRegex);
Function<String, SearchMatch> function = createForwardMatchFunction(pattern);
FieldLocation start = new FieldLocation();
List<SearchLocation> results = new ArrayList<>();
DecompilerSearchLocation searchLocation = findNext(function, searchString, start);
while (searchLocation != null) {
results.add(searchLocation);
FieldLocation last = searchLocation.getFieldLocation();
int line = last.getIndex().intValue();
int field = 0; // there is only 1 field
int row = 0; // there is only 1 row
int col = last.getCol() + 1; // move over one char to handle sub-matches
start = new FieldLocation(line, field, row, col);
searchLocation = findNext(function, searchString, start);
}
return results;
}
private DecompilerSearchLocation doFind(String searchString, FieldLocation currentLocation,
boolean forwardSearch, boolean isRegex) {
Pattern pattern = createPattern(searchString, isRegex);
if (forwardSearch) {
Function<String, SearchMatch> function = createForwardMatchFunction(pattern);
return findNext(function, searchString, currentLocation);
}
Function<String, SearchMatch> reverse = createReverseMatchFunction(pattern);
return findPrevious(reverse, searchString, currentLocation);
}
private Pattern createPattern(String searchString, boolean isRegex) {
int options = Pattern.CASE_INSENSITIVE | Pattern.DOTALL;
if (isRegex) {
try {
return Pattern.compile(searchString, options);
}
catch (PatternSyntaxException e) {
Msg.showError(this, decompilerPanel, "Regular Expression Syntax Error",
e.getMessage());
return null;
}
}
return UserSearchUtils.createPattern(searchString, false, options);
}
private Function<String, SearchMatch> createForwardMatchFunction(Pattern pattern) {
return textLine -> {
Matcher matcher = pattern.matcher(textLine);
if (matcher.find()) {
int start = matcher.start();
int end = matcher.end();
return new SearchMatch(start, end, textLine);
}
return SearchMatch.NO_MATCH;
};
}
private Function<String, SearchMatch> createReverseMatchFunction(Pattern pattern) {
return textLine -> {
Matcher matcher = pattern.matcher(textLine);
if (!matcher.find()) {
return SearchMatch.NO_MATCH;
}
int start = matcher.start();
int end = matcher.end();
// Since the matcher can only match from the start to end of line, we need to find all
// matches and then take the last match
// Setting the region to one character past the previous match allows repeated matches
// within a match. The default behavior of the matcher is to start the match after
// the previous match found by find().
matcher.region(start + 1, textLine.length());
while (matcher.find()) {
start = matcher.start();
end = matcher.end();
matcher.region(start + 1, textLine.length());
}
return new SearchMatch(start, end, textLine);
};
}
private DecompilerSearchLocation findNext(Function<String, SearchMatch> matcher,
String searchString, FieldLocation currentLocation) {
List<Field> fields = decompilerPanel.getFields();
int line = currentLocation.getIndex().intValue();
for (int i = line; i < fields.size(); i++) {
ClangTextField field = (ClangTextField) fields.get(i);
String partialLine = substring(field, (i == line) ? currentLocation : null, true);
SearchMatch match = matcher.apply(partialLine);
if (match == SearchMatch.NO_MATCH) {
continue;
}
if (i == line) { // cursor is on this line
//
// The match start for all lines without the cursor will be relative to the start
// of the line, which is 0. However, when searching on the row with the cursor,
// the match start is relative to the cursor position. Update the start to
// compensate for the difference between the start of the line and the cursor.
//
String fullLine = field.getText();
int cursorOffset = fullLine.length() - partialLine.length();
match.start += cursorOffset;
match.end += cursorOffset;
}
FieldLineLocation lineInfo = getFieldIndexFromOffset(match.start, field);
FieldLocation fieldLocation =
new FieldLocation(i, lineInfo.fieldNumber(), 0, lineInfo.column());
return new DecompilerSearchLocation(fieldLocation, match.start, match.end - 1,
searchString, true, field.getText());
}
return null;
}
private DecompilerSearchLocation findPrevious(Function<String, SearchMatch> matcher,
String searchString, FieldLocation currentLocation) {
List<Field> fields = decompilerPanel.getFields();
int line = currentLocation.getIndex().intValue();
for (int i = line; i >= 0; i--) {
ClangTextField field = (ClangTextField) fields.get(i);
String textLine = substring(field, (i == line) ? currentLocation : null, false);
SearchMatch match = matcher.apply(textLine);
if (match != SearchMatch.NO_MATCH) {
FieldLineLocation lineInfo = getFieldIndexFromOffset(match.start, field);
FieldLocation fieldLocation =
new FieldLocation(i, lineInfo.fieldNumber(), 0, lineInfo.column());
return new DecompilerSearchLocation(fieldLocation, match.start, match.end - 1,
searchString, false, field.getText());
}
}
return null;
}
private String substring(ClangTextField textField, FieldLocation location,
boolean forwardSearch) {
if (location == null) { // the cursor location is not on this line; use all of the text
return textField.getText();
}
if (textField.getText().isEmpty()) { // the cursor is on blank line
return "";
}
String partialText = textField.getText();
if (forwardSearch) {
int nextCol = location.getCol();
// protects against the location column being out of range (this can happen if we're
// searching forward and the cursor is past the last token)
if (nextCol >= partialText.length()) {
return "";
}
// skip a character to start the next search; this prevents matching the previous match
return partialText.substring(nextCol);
}
// backwards search
return partialText.substring(0, location.getCol());
}
private FieldLineLocation getFieldIndexFromOffset(int screenOffset, ClangTextField textField) {
RowColLocation rowColLocation = textField.textOffsetToScreenLocation(screenOffset);
// we use 0 here because currently there is only one field, which is the entire line
return new FieldLineLocation(0, rowColLocation.col());
}
private static class SearchMatch {
private static SearchMatch NO_MATCH = new SearchMatch(-1, -1, null);
private int start;
private int end;
private String textLine;
SearchMatch(int start, int end, String textLine) {
this.start = start;
this.end = end;
this.textLine = textLine;
}
@Override
public String toString() {
if (this == NO_MATCH) {
return "NO MATCH";
}
return "[start=" + start + ",end=" + end + "]: " + textLine;
}
}
private record FieldLineLocation(int fieldNumber, int column) {
}
}

View File

@ -23,13 +23,14 @@ import org.apache.commons.lang3.StringUtils;
import docking.action.KeyBindingData;
import docking.action.MenuData;
import docking.widgets.FindDialog;
import ghidra.app.decompiler.component.DecompilerFindDialog;
import ghidra.app.decompiler.component.DecompilerPanel;
import ghidra.app.plugin.core.decompile.DecompilerActionContext;
import ghidra.app.util.HelpTopics;
import ghidra.util.HelpLocation;
public class FindAction extends AbstractDecompilerAction {
private FindDialog findDialog;
private DecompilerFindDialog findDialog;
public FindAction() {
super("Find");
@ -49,15 +50,7 @@ public class FindAction extends AbstractDecompilerAction {
protected FindDialog getFindDialog(DecompilerPanel decompilerPanel) {
if (findDialog == null) {
findDialog =
new FindDialog("Decompiler Find Text", new DecompilerSearcher(decompilerPanel)) {
@Override
protected void dialogClosed() {
// clear the search results when the dialog is closed
decompilerPanel.setSearchResults(null);
}
};
findDialog.setHelpLocation(new HelpLocation(HelpTopics.DECOMPILER, "ActionFind"));
findDialog = new DecompilerFindDialog(decompilerPanel);
}
return findDialog;
}

View File

@ -17,21 +17,28 @@ package ghidra.app.plugin.core.decompile;
import static org.junit.Assert.*;
import java.util.List;
import org.junit.After;
import org.junit.Test;
import docking.action.DockingActionIf;
import docking.widgets.FindDialog;
import docking.widgets.dialogs.InputDialog;
import docking.widgets.fieldpanel.support.FieldLocation;
import docking.widgets.table.GTable;
import ghidra.app.decompiler.ClangToken;
import ghidra.app.decompiler.component.DecompilerFindDialog;
import ghidra.app.decompiler.component.DecompilerPanel;
import ghidra.app.plugin.core.decompile.actions.FieldBasedSearchLocation;
import ghidra.app.plugin.core.decompile.actions.DecompilerSearchLocation;
import ghidra.app.plugin.core.table.TableComponentProvider;
import ghidra.program.model.listing.Program;
import ghidra.test.ClassicSampleX86ProgramBuilder;
import ghidra.util.table.GhidraProgramTableModel;
import ghidra.util.table.GhidraThreadedTablePanel;
public class DecompilerFindDialogTest extends AbstractDecompilerTest {
private FindDialog findDialog;
private DecompilerFindDialog findDialog;
@Override
@After
@ -243,10 +250,78 @@ public class DecompilerFindDialogTest extends AbstractDecompilerTest {
assertSearchHit(line, column, length);
}
@Test
public void testSearchAll() {
/*
bool FUN_01002239(int param_1)
{
undefined4 uVar1;
int iVar2;
undefined4 *puVar3;
bool bVar4;
undefined *puVar5;
undefined2 local_210;
undefined4 local_20e [129];
int local_8;
local_210 = 0;
puVar3 = local_20e;
...
...
...
*/
decompile("1002239");
String text = "puVar";
showFind(text);
searchAll();
GTable table = getResultsTable();
List<DecompilerSearchLocation> results = getResults(table);
assertEquals(10, results.size());
// click some rows and verify the cursor location
for (int i = 0; i < results.size(); i++) {
clickAndVerify(i, table, results);
}
}
//==================================================================================================
// Private Methods
//==================================================================================================
private void clickAndVerify(int row, GTable table, List<DecompilerSearchLocation> results) {
runSwing(() -> table.selectRow(row));
DecompilerSearchLocation searchLocation = results.get(row);
FieldLocation fieldLocation = searchLocation.getFieldLocation();
ClangToken expectedToken = getToken(fieldLocation);
ClangToken cursorToken = getToken();
assertEquals(expectedToken, cursorToken);
}
private GTable getResultsTable() {
@SuppressWarnings("unchecked")
TableComponentProvider<DecompilerSearchLocation> tableProvider =
waitForComponentProvider(TableComponentProvider.class);
GhidraThreadedTablePanel<DecompilerSearchLocation> panel =
tableProvider.getThreadedTablePanel();
return panel.getTable();
}
private List<DecompilerSearchLocation> getResults(GTable table) {
@SuppressWarnings("unchecked")
GhidraProgramTableModel<DecompilerSearchLocation> model =
(GhidraProgramTableModel<DecompilerSearchLocation>) table.getModel();
waitForTableModel(model);
return model.getModelData();
}
private void next() {
runSwing(() -> findDialog.next());
}
@ -255,13 +330,17 @@ public class DecompilerFindDialogTest extends AbstractDecompilerTest {
runSwing(() -> findDialog.previous());
}
private void searchAll() {
pressButtonByText(findDialog, "Search All");
}
private void assertSearchHit(int line, int column, int length) {
waitForSwing();
assertCurrentLocation(line, column);
DecompilerPanel panel = getDecompilerPanel();
FieldBasedSearchLocation searchResults = panel.getSearchResults();
DecompilerSearchLocation searchResults = panel.getSearchResults();
FieldLocation searchCursorLocation = searchResults.getFieldLocation();
int searchLineNumber = searchCursorLocation.getIndex().intValue() + 1;
assertEquals("Search result is on the wrong line", line, searchLineNumber);
@ -285,7 +364,7 @@ public class DecompilerFindDialogTest extends AbstractDecompilerTest {
private void showFind(String text) {
DockingActionIf findAction = getAction(decompiler, "Find");
performAction(findAction, provider, true);
findDialog = waitForDialogComponent(FindDialog.class);
findDialog = waitForDialogComponent(DecompilerFindDialog.class);
runSwing(() -> findDialog.setSearchText(text));
}

View File

@ -216,7 +216,7 @@ public class ThemeIconTableModel extends GDynamicColumnTableModel<IconValue, Obj
private class ThemeIconRenderer extends AbstractGColumnRenderer<ResolvedIcon> {
public ThemeIconRenderer() {
setFont(Gui.getFont("font.monospaced"));
setBaseFontId("font.monospaced");
}
@Override

View File

@ -32,7 +32,7 @@ public class FindDialog extends ReusableDialogComponentProvider {
private GhidraComboBox<String> comboBox;
private FindDialogSearcher searcher;
protected FindDialogSearcher searcher;
private JButton nextButton;
private JButton previousButton;
private JRadioButton stringRadioButton;
@ -140,6 +140,10 @@ public class FindDialog extends ReusableDialogComponentProvider {
doSearch(false);
}
protected boolean useRegex() {
return regexRadioButton.isSelected();
}
private void doSearch(boolean forward) {
if (!nextButton.isEnabled()) {
@ -190,7 +194,7 @@ public class FindDialog extends ReusableDialogComponentProvider {
notifyUser("Not found");
}
private void notifySearchHit(SearchLocation location) {
protected void notifySearchHit(SearchLocation location) {
searcher.setCursorPosition(location.getCursorPosition());
storeSearchText(location.getSearchText());
searcher.highlightSearchResults(location);
@ -234,7 +238,7 @@ public class FindDialog extends ReusableDialogComponentProvider {
history.forEach(comboBox::addToModel);
}
private void storeSearchText(String text) {
protected void storeSearchText(String text) {
MutableComboBoxModel<String> model = (MutableComboBoxModel<String>) comboBox.getModel();
model.insertElementAt(text, 0);

View File

@ -15,21 +15,72 @@
*/
package docking.widgets;
import java.util.List;
import javax.help.UnsupportedOperationException;
/**
* A simple interface for the {@link FindDialog} so that it can work for different search clients.
* <p>
* The {@link CursorPosition} object used by this interface is one that implementations can extend
* to add extra context to use when searching. The implementation is responsible for creating the
* locations and these locations will later be handed back to the searcher.
*/
public interface FindDialogSearcher {
/**
* The current cursor position. Used to search for the next item.
* @return the cursor position.
*/
public CursorPosition getCursorPosition();
/**
* Sets the cursor position after a successful search.
* @param position the cursor position.
*/
public void setCursorPosition(CursorPosition position);
/**
* Returns the start cursor position. This is used when a search is wrapped to start at the
* beginning of the search range.
* @return the start position.
*/
public CursorPosition getStart();
/**
* The end cursor position. This is used when a search is wrapped while searching backwards to
* start at the end position.
* @return the end position.
*/
public CursorPosition getEnd();
/**
* Called to signal the implementor should highlight the given search location.
* @param location the search result location.
*/
public void highlightSearchResults(SearchLocation location);
/**
* Perform a search for the next item in the given direction starting at the given cursor
* position.
* @param text the search text.
* @param cursorPosition the current cursor position.
* @param searchForward true if searching forward.
* @param useRegex useRegex true if the search text is a regular expression; false if the texts is
* literal text.
* @return the search result or null if no match was found.
*/
public SearchLocation search(String text, CursorPosition cursorPosition, boolean searchForward,
boolean useRegex);
/**
* Search for all matches.
* @param text the search text.
* @param useRegex true if the search text is a regular expression; false if the texts is
* literal text.
* @return all search results or an empty list.
*/
public default List<SearchLocation> searchAll(String text, boolean useRegex) {
throw new UnsupportedOperationException("Search All is not defined for this searcher");
}
}

View File

@ -21,7 +21,7 @@ import javax.swing.JButton;
import resources.ResourceManager;
/**
* A drop-in replacement for {@link JButton} that correctly installs a disable icon.
* A drop-in replacement for {@link JButton} that correctly installs a disabled icon.
*/
public class GButton extends JButton {