GP-6076 - Find All - Added a button to the Find dialog to find all matches and show the results in a table

This commit is contained in:
dragonmacher
2025-11-12 12:34:30 -05:00
parent 2597c243b9
commit 2ae42befb8
63 changed files with 3131 additions and 1273 deletions

View File

@@ -1054,7 +1054,6 @@ src/main/resources/images/small_minus.png||GHIDRA||||END|
src/main/resources/images/small_plus.png||GHIDRA||||END|
src/main/resources/images/stopNode.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
src/main/resources/images/sync_enabled.png||GHIDRA||||END|
src/main/resources/images/table_delete.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
src/main/resources/images/table_go.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
src/main/resources/images/table_row_delete.png||FAMFAMFAM Icons - CC 2.5|||silk|END|
src/main/resources/images/tag_blue.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|

View File

@@ -324,7 +324,6 @@ icon.plugin.symboltable.referencetable.provider = table_go.png
icon.plugin.table.service = icon.search
icon.plugin.table.service.marker = icon.base.search.marker
icon.plugin.table.delete.row = table_delete.png
icon.plugin.totd.provider = help-hint.png

View File

@@ -108,7 +108,7 @@
<IMG src= "images/ProjectDataTree.png" border="0">
</CENTER>
<BLOCKQUOTE>
<P>The data tree shows all files in the project orgnanized into folders and sub-folders.
<P>The data tree shows all files in the project organized into folders and sub-folders.
<A href="#FileIcons">Icons for files</A> indicate whether they are under <A href=
"help/topics/VersionControl/project_repository.htm#Versioning">version control</A> and whether
you have the file <A href=

View File

@@ -26,7 +26,7 @@ import docking.*;
import docking.action.*;
import docking.action.builder.ActionBuilder;
import docking.widgets.FindDialog;
import docking.widgets.TextComponentSearcher;
import docking.widgets.search.TextComponentSearcher;
import generic.theme.GIcon;
import generic.theme.Gui;
import ghidra.app.script.DecoratingPrintWriter;
@@ -69,7 +69,6 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
private Address currentAddress;
private FindDialog findDialog;
private TextComponentSearcher searcher;
public ConsoleComponentProvider(PluginTool tool, String owner) {
super(tool, "Console", owner);
@@ -192,8 +191,8 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
private void showFindDialog() {
if (findDialog == null) {
searcher = new TextComponentSearcher(textPane);
findDialog = new FindDialog("Find", searcher);
TextComponentSearcher searcher = new TextComponentSearcher(textPane);
findDialog = new FindDialog("Console Find", searcher);
}
getTool().showDialog(findDialog);
}
@@ -220,8 +219,8 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
checkVisible();
textPane.setText("");
if (searcher != null) {
searcher.clearHighlights();
if (findDialog != null) {
findDialog.close(); // this will also dispose of any search highlights
}
}

View File

@@ -25,7 +25,9 @@ import docking.ActionContext;
import docking.action.DockingAction;
import docking.action.ToolBarData;
import docking.action.builder.ActionBuilder;
import docking.widgets.*;
import docking.widgets.FindDialog;
import docking.widgets.OptionDialog;
import docking.widgets.search.TextComponentSearcher;
import generic.theme.GIcon;
import ghidra.app.util.HelpTopics;
import ghidra.framework.plugintool.ComponentProviderAdapter;
@@ -41,7 +43,6 @@ public class InterpreterComponentProvider extends ComponentProviderAdapter
private List<Callback> firstActivationCallbacks;
private FindDialog findDialog;
private TextComponentSearcher searcher;
public InterpreterComponentProvider(InterpreterPanelPlugin plugin,
InterpreterConnection interpreter, boolean visible) {
@@ -95,8 +96,8 @@ public class InterpreterComponentProvider extends ComponentProviderAdapter
private void showFindDialog() {
if (findDialog == null) {
JTextPane textPane = panel.getOutputTextPane();
searcher = new TextComponentSearcher(textPane);
findDialog = new FindDialog("Find", searcher);
TextComponentSearcher searcher = new TextComponentSearcher(textPane);
findDialog = new FindDialog("Intepreter Find", searcher);
}
getTool().showDialog(findDialog);
}
@@ -154,8 +155,8 @@ public class InterpreterComponentProvider extends ComponentProviderAdapter
public void clear() {
panel.clear();
if (searcher != null) {
searcher.clearHighlights();
if (findDialog != null) {
findDialog.close(); // this will also dispose of any search highlights
}
}

View File

@@ -15,10 +15,11 @@
*/
package ghidra.app.plugin.core.navigation.locationreferences;
import static ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext.*;
import static docking.widgets.search.SearchLocationContext.*;
import java.util.Objects;
import docking.widgets.search.SearchLocationContext;
import ghidra.program.model.address.Address;
import ghidra.program.model.symbol.DynamicReference;
import ghidra.program.model.symbol.Reference;
@@ -34,7 +35,7 @@ public class LocationReference implements Comparable<LocationReference> {
private final boolean isOffcutReference;
private final Address locationOfUseAddress;
private final String refType;
private final LocationReferenceContext context;
private final SearchLocationContext context;
private final ProgramLocation location;
/**
@@ -52,7 +53,7 @@ public class LocationReference implements Comparable<LocationReference> {
// represents the 'from' address for a reference; for parameters and variables of a
// function, this represents the address of that variable.
private LocationReference(Address address, ProgramLocation location, String refType,
LocationReferenceContext context, boolean isOffcut) {
SearchLocationContext context, boolean isOffcut) {
this.locationOfUseAddress = Objects.requireNonNull(address);
this.location = location;
this.refType = refType == null ? "" : refType;
@@ -75,15 +76,15 @@ public class LocationReference implements Comparable<LocationReference> {
}
LocationReference(Address locationOfUseAddress, String context) {
this(locationOfUseAddress, null, null, LocationReferenceContext.get(context), false);
this(locationOfUseAddress, null, null, SearchLocationContext.get(context), false);
}
LocationReference(Address locationOfUseAddress, LocationReferenceContext context) {
this(locationOfUseAddress, null, null, LocationReferenceContext.get(context), false);
LocationReference(Address locationOfUseAddress, SearchLocationContext context) {
this(locationOfUseAddress, null, null, SearchLocationContext.get(context), false);
}
LocationReference(Address locationOfUseAddress, String context, ProgramLocation location) {
this(locationOfUseAddress, location, null, LocationReferenceContext.get(context), false);
this(locationOfUseAddress, location, null, SearchLocationContext.get(context), false);
}
/**
@@ -128,7 +129,7 @@ public class LocationReference implements Comparable<LocationReference> {
*
* @return the context
*/
public LocationReferenceContext getContext() {
public SearchLocationContext getContext() {
return context;
}

View File

@@ -21,6 +21,7 @@ import java.util.function.Supplier;
import docking.ActionContext;
import docking.action.DockingAction;
import docking.action.builder.ActionBuilder;
import docking.widgets.table.actions.DeleteTableRowAction;
import ghidra.app.CorePluginPackage;
import ghidra.app.context.ListingActionContext;
import ghidra.app.events.ProgramClosedPluginEvent;
@@ -39,7 +40,6 @@ import ghidra.program.model.symbol.Reference;
import ghidra.program.util.ProgramLocation;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
import ghidra.util.table.actions.DeleteTableRowAction;
/**
* Plugin to show a list of references to the item represented by the location of the cursor.

View File

@@ -27,6 +27,7 @@ import docking.ActionContext;
import docking.action.*;
import docking.action.builder.ActionBuilder;
import docking.widgets.table.GTable;
import docking.widgets.table.actions.DeleteTableRowAction;
import generic.theme.GIcon;
import ghidra.app.cmd.refs.RemoveReferenceCmd;
import ghidra.app.nav.Navigatable;
@@ -45,7 +46,6 @@ import ghidra.program.util.ProgramLocation;
import ghidra.util.HelpLocation;
import ghidra.util.table.GhidraTable;
import ghidra.util.table.SelectionNavigationAction;
import ghidra.util.table.actions.DeleteTableRowAction;
import ghidra.util.table.actions.MakeProgramSelectionAction;
import ghidra.util.task.SwingUpdateManager;
import resources.Icons;

View File

@@ -4,9 +4,9 @@
* 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.
@@ -20,6 +20,7 @@ import java.util.*;
import org.apache.commons.lang3.StringUtils;
import docking.widgets.search.SearchLocationContext;
import docking.widgets.table.GTableCellRenderingData;
import ghidra.docking.settings.Settings;
import ghidra.framework.plugintool.ServiceProvider;
@@ -176,7 +177,7 @@ class LocationReferencesTableModel extends AddressBasedTableModel<LocationRefere
}
// when the row object does not represent an applied reference, then it may have context
LocationReferenceContext context = rowObject.getContext();
SearchLocationContext context = rowObject.getContext();
String text = context.getBoldMatchingText();
setText(text);
return this;
@@ -202,7 +203,7 @@ class LocationReferencesTableModel extends AddressBasedTableModel<LocationRefere
return refTypeString;
}
LocationReferenceContext context = rowObject.getContext();
SearchLocationContext context = rowObject.getContext();
return context.getPlainText();
}
}

View File

@@ -20,6 +20,7 @@ import java.util.Stack;
import java.util.function.Consumer;
import java.util.function.Predicate;
import docking.widgets.search.SearchLocationContext;
import ghidra.app.services.*;
import ghidra.program.model.address.*;
import ghidra.program.model.data.*;
@@ -343,7 +344,7 @@ public final class ReferenceUtils {
Consumer<DataTypeReference> callback = ref -> {
LocationReferenceContext context = ref.getContext();
SearchLocationContext context = ref.getContext();
LocationReference locationReference = new LocationReference(ref.getAddress(), context);
accumulator.add(locationReference);
};

View File

@@ -4,9 +4,9 @@
* 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.
@@ -23,6 +23,7 @@ import java.util.*;
import docking.action.DockingAction;
import docking.action.MenuData;
import docking.tool.ToolConstants;
import docking.widgets.table.actions.DeleteTableRowAction;
import ghidra.app.CorePluginPackage;
import ghidra.app.context.NavigatableActionContext;
import ghidra.app.context.NavigatableContextAction;
@@ -37,7 +38,6 @@ import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.program.model.listing.Program;
import ghidra.util.HelpLocation;
import ghidra.util.table.actions.DeleteTableRowAction;
import ghidra.util.task.SwingUpdateManager;
/**

View File

@@ -26,6 +26,7 @@ import docking.*;
import docking.widgets.label.GLabel;
import docking.widgets.table.GTableFilterPanel;
import docking.widgets.table.TableFilter;
import docking.widgets.table.actions.DeleteTableRowAction;
import generic.theme.GIcon;
import ghidra.app.plugin.core.scalartable.RangeFilterTextField.FilterType;
import ghidra.framework.plugintool.ComponentProviderAdapter;
@@ -35,7 +36,6 @@ import ghidra.program.model.scalar.Scalar;
import ghidra.program.util.ProgramSelection;
import ghidra.util.HelpLocation;
import ghidra.util.table.*;
import ghidra.util.table.actions.DeleteTableRowAction;
import ghidra.util.table.actions.MakeProgramSelectionAction;
import help.HelpService;

View File

@@ -31,6 +31,7 @@ import docking.action.DockingAction;
import docking.action.MenuData;
import docking.widgets.table.AbstractSortedTableModel;
import docking.widgets.table.GTable;
import docking.widgets.table.actions.DeleteTableRowAction;
import generic.theme.GIcon;
import ghidra.app.nav.Navigatable;
import ghidra.app.nav.NavigatableRemovalListener;
@@ -44,7 +45,6 @@ import ghidra.program.model.listing.Program;
import ghidra.program.util.*;
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;

View File

@@ -20,6 +20,7 @@ import java.util.*;
import javax.swing.Icon;
import docking.widgets.table.actions.DeleteTableRowAction;
import ghidra.app.CorePluginPackage;
import ghidra.app.events.ProgramClosedPluginEvent;
import ghidra.app.nav.Navigatable;
@@ -38,7 +39,6 @@ import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.program.model.listing.Program;
import ghidra.util.Swing;
import ghidra.util.table.GhidraProgramTableModel;
import ghidra.util.table.actions.DeleteTableRowAction;
import ghidra.util.task.SwingUpdateManager;
//@formatter:off
@@ -170,9 +170,7 @@ public class TableServicePlugin extends ProgramPlugin
}
void remove(TableComponentProvider<?> provider) {
Iterator<Program> iter = programMap.keySet().iterator();
while (iter.hasNext()) {
Program p = iter.next();
for (Program p : programMap.keySet()) {
List<TableComponentProvider<?>> list = programMap.get(p);
if (list.remove(provider)) {
if (list.size() == 0) {
@@ -184,9 +182,7 @@ public class TableServicePlugin extends ProgramPlugin
}
void removeDialog(TableServiceTableChooserDialog dialog) {
Iterator<Program> iter = programToDialogMap.keySet().iterator();
while (iter.hasNext()) {
Program p = iter.next();
for (Program p : programToDialogMap.keySet()) {
List<TableChooserDialog> list = programToDialogMap.get(p);
if (list.remove(dialog)) {
if (list.size() == 0) {
@@ -213,9 +209,7 @@ public class TableServicePlugin extends ProgramPlugin
private List<TableComponentProvider<?>> getProviders() {
List<TableComponentProvider<?>> clist = new ArrayList<>();
Iterator<List<TableComponentProvider<?>>> iter = programMap.values().iterator();
while (iter.hasNext()) {
List<TableComponentProvider<?>> list = iter.next();
for (List<TableComponentProvider<?>> list : programMap.values()) {
clist.addAll(list);
}
return clist;

View File

@@ -4,9 +4,9 @@
* 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.
@@ -15,7 +15,7 @@
*/
package ghidra.app.services;
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext;
import docking.widgets.search.SearchLocationContext;
import ghidra.program.model.address.Address;
import ghidra.program.model.data.DataType;
import ghidra.program.model.listing.Function;
@@ -31,10 +31,10 @@ public class DataTypeReference {
private Address address;
/** A preview of how the reference was used */
private LocationReferenceContext context;
private SearchLocationContext context;
public DataTypeReference(DataType dataType, String fieldName, Function function,
Address address, LocationReferenceContext context) {
Address address, SearchLocationContext context) {
this.dataType = dataType;
this.fieldName = fieldName;
this.function = function;
@@ -54,7 +54,7 @@ public class DataTypeReference {
return address;
}
public LocationReferenceContext getContext() {
public SearchLocationContext getContext() {
return context;
}

View File

@@ -33,6 +33,7 @@ import docking.action.builder.ToggleActionBuilder;
import docking.util.GGlassPaneMessage;
import docking.widgets.OptionDialog;
import docking.widgets.OptionDialogBuilder;
import docking.widgets.table.actions.DeleteTableRowAction;
import generic.theme.GIcon;
import ghidra.app.context.NavigatableActionContext;
import ghidra.app.nav.Navigatable;
@@ -57,7 +58,6 @@ import ghidra.util.Msg;
import ghidra.util.layout.VerticalLayout;
import ghidra.util.table.GhidraTable;
import ghidra.util.table.SelectionNavigationAction;
import ghidra.util.table.actions.DeleteTableRowAction;
import ghidra.util.table.actions.MakeProgramSelectionAction;
import resources.Icons;

View File

@@ -29,6 +29,7 @@ import docking.action.ToggleDockingAction;
import docking.action.builder.ActionBuilder;
import docking.action.builder.ToggleActionBuilder;
import docking.widgets.table.GTable;
import docking.widgets.table.actions.DeleteTableRowAction;
import docking.widgets.table.threaded.ThreadedTableModel;
import generic.theme.GIcon;
import ghidra.app.nav.Navigatable;
@@ -38,7 +39,6 @@ import ghidra.program.model.listing.Program;
import ghidra.program.util.ProgramTask;
import ghidra.util.HelpLocation;
import ghidra.util.table.*;
import ghidra.util.table.actions.DeleteTableRowAction;
import ghidra.util.table.actions.MakeProgramSelectionAction;
import ghidra.util.task.TaskLauncher;
import ghidra.util.task.TaskMonitor;

View File

@@ -281,6 +281,8 @@ public class GhidraTable extends GTable {
* <p>
* This method differs from {@link #navigate(int, int)} in that this method will not navigate if
* {@link #navigateOnSelection} is <code>false</code>.
* @param row the row
* @param column the column
*/
protected void navigateOnCurrentSelection(int row, int column) {
if (!navigateOnSelection) {

View File

@@ -31,8 +31,9 @@ import org.junit.*;
import docking.action.DockingActionIf;
import docking.util.AnimationUtils;
import docking.widgets.FindDialog;
import docking.widgets.TextComponentSearcher;
import docking.widgets.*;
import docking.widgets.search.TextComponentSearcher;
import docking.widgets.table.GTable;
import generic.jar.ResourceFile;
import generic.theme.GColor;
import ghidra.app.script.GhidraScript;
@@ -78,8 +79,6 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
placeCursorAtBeginning();
findDialog = showFindDialog();
String searchText = "Hello";
find(searchText);
}
@After
@@ -88,12 +87,47 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
env.dispose();
}
@Test
public void testFindAll() throws Exception {
String searchText = "Hello";
findAll(searchText);
assertFalse(findDialog.isShowing());
List<TestTextMatch> matches = getExpectedMatches(searchText);
assertEquals(3, matches.size());
verfiyHighlightColor(matches);
FindDialogResultsProvider resultsProvider =
waitForComponentProvider(FindDialogResultsProvider.class);
List<SearchLocation> results = resultsProvider.getResults();
assertEquals(matches.size(), results.size());
int row = 0;
selectRow(resultsProvider, row);
assertActiveHighlight(matches.get(row));
assertCursorInMatch(matches.get(row));
row = 1;
selectRow(resultsProvider, row);
assertActiveHighlight(matches.get(row));
assertCursorInMatch(matches.get(row));
row = 2;
selectRow(resultsProvider, row);
assertActiveHighlight(matches.get(row));
assertCursorInMatch(matches.get(row));
}
@Test
public void testFindHighlights() throws Exception {
List<TestTextMatch> matches = getMatches();
String searchText = "Hello";
find(searchText);
List<TestTextMatch> matches = getExpectedMatches();
assertEquals(3, matches.size());
verfyHighlightColor(matches);
verfiyHighlightColor(matches);
close(findDialog);
verifyDefaultBackgroundColorForAllText();
@@ -102,18 +136,21 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
@Test
public void testFindHighlights_ChangeSearchText() throws Exception {
List<TestTextMatch> matches = getMatches();
String searchText = "Hello";
find(searchText);
List<TestTextMatch> matches = getExpectedMatches();
assertEquals(3, matches.size());
verfyHighlightColor(matches);
verfiyHighlightColor(matches);
// Change the search text after the first search and make sure the new text is found and
// highlighted correctly.
String newSearchText = "java";
runSwing(() -> findDialog.setSearchText(newSearchText));
pressButtonByText(findDialog, "Next");
matches = getMatches();
matches = getExpectedMatches();
assertEquals(2, matches.size());
verfyHighlightColor(matches);
verfiyHighlightColor(matches);
close(findDialog);
verifyDefaultBackgroundColorForAllText();
@@ -122,9 +159,12 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
@Test
public void testFindHighlights_ChangeDocumentText() throws Exception {
List<TestTextMatch> matches = getMatches();
String searchText = "Hello";
find(searchText);
List<TestTextMatch> matches = getExpectedMatches();
assertEquals(3, matches.size());
verfyHighlightColor(matches);
verfiyHighlightColor(matches);
runSwing(() -> textPane.setText("This is some\nnew text."));
@@ -135,7 +175,10 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
@Test
public void testMovingCursorUpdatesActiveHighlight() {
List<TestTextMatch> matches = getMatches();
String searchText = "Hello";
find(searchText);
List<TestTextMatch> matches = getExpectedMatches();
assertEquals(3, matches.size());
TestTextMatch first = matches.get(0);
TestTextMatch second = matches.get(1);
@@ -154,7 +197,10 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
@Test
public void testFindNext_ChangeDocumentText() throws Exception {
List<TestTextMatch> matches = getMatches();
String searchText = "Hello";
find(searchText);
List<TestTextMatch> matches = getExpectedMatches();
assertEquals(3, matches.size());
TestTextMatch first = matches.get(0);
TestTextMatch second = matches.get(1);
@@ -177,11 +223,11 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
// worth worrying about.)
next();
matches = getMatches();
matches = getExpectedMatches();
assertEquals(5, matches.size()); // 3 old matches plus 2 new matches
second = matches.get(1);
assertCursorInMatch(second);
assertActiveHighlight(second);
TestTextMatch third = matches.get(1);
assertCursorInMatch(third);
assertActiveHighlight(third);
next(); // third
next(); // fourth
@@ -193,10 +239,28 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
close(findDialog);
}
@Test
public void testFindNext_NoMatches() throws Exception {
String searchText = "Goodbye";
find(searchText);
List<TestTextMatch> matches = getExpectedMatches();
assertEquals(0, matches.size());
String status = runSwing(() -> findDialog.getStatusText());
assertEquals("Not found", status);
runSwing(() -> findDialog.dispose());
}
@Test
public void testFindNext() throws Exception {
List<TestTextMatch> matches = getMatches();
String searchText = "Hello";
find(searchText);
List<TestTextMatch> matches = getExpectedMatches();
assertEquals(3, matches.size());
TestTextMatch first = matches.get(0);
TestTextMatch second = matches.get(1);
@@ -224,7 +288,10 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
@Test
public void testFindNext_MoveCaret() throws Exception {
List<TestTextMatch> matches = getMatches();
String searchText = "Hello";
find(searchText);
List<TestTextMatch> matches = getExpectedMatches();
assertEquals(3, matches.size());
TestTextMatch first = matches.get(0);
TestTextMatch second = matches.get(1);
@@ -247,7 +314,10 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
@Test
public void testFindPrevious() throws Exception {
List<TestTextMatch> matches = getMatches();
String searchText = "Hello";
find(searchText);
List<TestTextMatch> matches = getExpectedMatches();
assertEquals(3, matches.size());
TestTextMatch first = matches.get(0);
TestTextMatch second = matches.get(1);
@@ -277,7 +347,10 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
@Test
public void testFindPrevious_MoveCaret() throws Exception {
List<TestTextMatch> matches = getMatches();
String searchText = "Hello";
find(searchText);
List<TestTextMatch> matches = getExpectedMatches();
assertEquals(3, matches.size());
TestTextMatch first = matches.get(0);
TestTextMatch second = matches.get(1);
@@ -300,9 +373,12 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
@Test
public void testClear() throws Exception {
List<TestTextMatch> matches = getMatches();
String searchText = "Hello";
find(searchText);
List<TestTextMatch> matches = getExpectedMatches();
assertEquals(3, matches.size());
verfyHighlightColor(matches);
verfiyHighlightColor(matches);
clear();
@@ -331,17 +407,26 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
private void next() {
pressButtonByText(findDialog, "Next");
waitForSwing();
waitForTasks();
}
private void previous() {
pressButtonByText(findDialog, "Previous");
waitForSwing();
waitForTasks();
}
private void findAll(String text) {
runSwing(() -> findDialog.setSearchText(text));
pressButtonByText(findDialog, "Find All");
waitForTasks();
}
private void assertSearchModelHasNoSearchResults() {
TextComponentSearcher searcher =
(TextComponentSearcher) findDialog.getSearcher();
if (searcher == null) {
return; // assume the searcher was disposed
}
assertFalse(searcher.hasSearchResults());
}
@@ -361,10 +446,11 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
private void assertActiveHighlight(TestTextMatch match) {
GColor expectedHlColor = new GColor("color.bg.find.highlight.active");
assertActiveHighlight(match, expectedHlColor);
assertActiveHighlight(match, expectedHlColor, true);
}
private void assertActiveHighlight(TestTextMatch match, Color expectedHlColor) {
private void assertActiveHighlight(TestTextMatch match, Color expectedHlColor,
boolean isActive) {
Highlight matchHighlight = runSwing(() -> {
Highlighter highlighter = textPane.getHighlighter();
@@ -379,10 +465,14 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
return null;
});
assertNotNull(matchHighlight);
assertNotNull("No highlight found for " + match, matchHighlight);
DefaultHighlightPainter painter = (DefaultHighlightPainter) matchHighlight.getPainter();
Color actualHlColor = painter.getColor();
assertEquals(expectedHlColor, actualHlColor);
String msg = "Expected %s highlight color for match %s".formatted(
isActive ? "active" : "inactive", match);
assertEquals(msg, expectedHlColor, actualHlColor);
}
private void placeCursorAtBeginning() {
@@ -396,7 +486,12 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
waitForSwing();
}
private void verfyHighlightColor(List<TestTextMatch> matches)
private void selectRow(FindDialogResultsProvider resultsProvider, int row) {
GTable table = resultsProvider.getTable();
runSwing(() -> table.selectRow(row));
}
private void verfiyHighlightColor(List<TestTextMatch> matches)
throws Exception {
GColor nonActiveHlColor = new GColor("color.bg.find.highlight");
@@ -405,10 +500,11 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
int caret = textPane.getCaretPosition();
for (TestTextMatch match : matches) {
Color expectedColor = nonActiveHlColor;
if (match.contains(caret)) {
boolean isActive = match.contains(caret);
if (isActive) {
expectedColor = activeHlColor;
}
assertActiveHighlight(match, expectedColor);
assertActiveHighlight(match, expectedColor, isActive);
}
}
@@ -426,9 +522,14 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
}
}
private List<TestTextMatch> getMatches() {
private List<TestTextMatch> getExpectedMatches() {
String searchText = findDialog.getSearchText();
assertFalse(searchText.isEmpty());
return getExpectedMatches(searchText);
}
private List<TestTextMatch> getExpectedMatches(String searchText) {
List<TestTextMatch> results = new ArrayList<>();
String text = runSwing(() -> textPane.getText());
@@ -449,8 +550,7 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
private void find(String text) {
runSwing(() -> findDialog.setSearchText(text));
pressButtonByText(findDialog, "Next");
waitForTasks();
next();
}
private FindDialog showFindDialog() {

View File

@@ -87,7 +87,7 @@ public class DecompilerDiffViewFindAction extends DockingAction {
private FindDialog createFindDialog(DecompilerPanel decompilerPanel, Side side) {
String title = (side == LEFT ? "Left" : "Right");
title += " Decompiler Find Text";
title += " Decompiler Find";
FindDialog dialog = new FindDialog(title, new DecompilerSearcher(decompilerPanel)) {
@Override
@@ -98,6 +98,15 @@ public class DecompilerDiffViewFindAction extends DockingAction {
};
dialog.setHelpLocation(new HelpLocation(HelpTopics.DECOMPILER, "ActionFind"));
/*
Find All will keep the results around in a separate window. When those results are
clicked, the Decompiler will update the current function. This can cause the function
comparison window to become out of sync with the available functions being compared. We
could update the function comparison to handle this case, but that doesn't seem worth it
at this time. For now, just disable the Find All.
*/
dialog.setFindAllEnabled(false);
return dialog;
}

View File

@@ -19,7 +19,8 @@ color.bg.decompiler.current.variable = color.palette.highlight.transparent.yello
color.bg.decompiler.highlights.middle.mouse = color.bg.highlight
color.bg.decompiler.highlights.default = color.palette.highlight.transparent.yellow
color.bg.decompiler.highlights.special = color.palette.crimson
color.bg.decompiler.highlights.find = color.palette.slateblue
color.bg.decompiler.highlights.find = color.palette.cornflowerblue
color.bg.decompiler.highlights.find.active = color.palette.lightskyblue
color.bg.decompiler.pcode.dfg.vertex.default = color.palette.red
color.bg.decompiler.pcode.dfg.vertex.selected = color.palette.lightcoral

View File

@@ -443,6 +443,7 @@ public class DecompileOptions {
private static final String SEARCH_HIGHLIGHT_MSG = "Display.Color for Highlighting Find Matches";
private static final GColor SEARCH_HIGHLIGHT_COLOR = new GColor("color.bg.decompiler.highlights.find");
private static final GColor SEARCH_HIGHLIGHT_ACTIVE_COLOR = new GColor("color.bg.decompiler.highlights.find.active");
private static final String HIGHLIGHT_MIDDLE_MOUSE_MSG = "Display.Color for Middle Mouse";
private static final GColor HIGHLIGHT_MIDDLE_MOUSE_COLOR = new GColor("color.bg.decompiler.highlights.middle.mouse");
@@ -1134,6 +1135,13 @@ public class DecompileOptions {
return HIGHLIGHT_MIDDLE_MOUSE_COLOR;
}
/**
* @return color used to highlight the active search result
*/
public Color getActiveSearchHighlightColor() {
return SEARCH_HIGHLIGHT_ACTIVE_COLOR;
}
/**
* @return color used to highlight search results
*/

View File

@@ -15,251 +15,16 @@
*/
package ghidra.app.decompiler.component;
import java.awt.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.*;
import ghidra.app.plugin.core.decompile.actions.DecompilerSearchLocation;
import ghidra.app.plugin.core.decompile.actions.DecompilerSearcher;
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext;
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.table.column.AbstractGhidraColumnRenderer;
import ghidra.util.table.column.GColumnRenderer;
import ghidra.util.task.TaskMonitor;
public class DecompilerFindDialog extends FindDialog {
private DecompilerPanel decompilerPanel;
private GButton showAllButton;
public DecompilerFindDialog(DecompilerPanel decompilerPanel) {
super("Decompiler Find Text", new DecompilerSearcher(decompilerPanel));
this.decompilerPanel = decompilerPanel;
super("Decompiler Find", new DecompilerSearcher(decompilerPanel));
setHelpLocation(new HelpLocation(HelpTopics.DECOMPILER, "ActionFind"));
showAllButton = new GButton("Search All");
showAllButton.addActionListener(e -> showAll());
// move this button to the end
removeButton(dismissButton);
addButton(showAllButton);
addButton(dismissButton);
}
@Override
protected void enableButtons(boolean b) {
super.enableButtons(b);
showAllButton.setEnabled(b);
}
private void showAll() {
String searchText = getSearchText();
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(searchText, 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 Results";
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()));
}
//=================================================================================================
// 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, LocationReferenceContext> {
private GColumnRenderer<LocationReferenceContext> renderer = new ContextCellRenderer();
@Override
public LocationReferenceContext getValue(DecompilerSearchLocation rowObject,
Settings settings,
ServiceProvider sp) throws IllegalArgumentException {
LocationReferenceContext context = rowObject.getContext();
return context;
// return rowObject.getTextLine();
}
@Override
public String getColumnName() {
return "Context";
}
@Override
public GColumnRenderer<LocationReferenceContext> getColumnRenderer() {
return renderer;
}
private class ContextCellRenderer
extends AbstractGhidraColumnRenderer<LocationReferenceContext> {
{
// the context uses html
setHTMLRenderingEnabled(true);
}
@Override
public Component getTableCellRendererComponent(GTableCellRenderingData data) {
// initialize
super.getTableCellRendererComponent(data);
DecompilerSearchLocation match = (DecompilerSearchLocation) data.getRowObject();
LocationReferenceContext context = match.getContext();
String text = context.getBoldMatchingText();
setText(text);
return this;
}
@Override
public String getFilterString(LocationReferenceContext context, Settings settings) {
return context.getPlainText();
}
}
}
}
}

View File

@@ -29,7 +29,6 @@ import docking.DockingUtils;
import docking.util.AnimationUtils;
import docking.util.SwingAnimationCallback;
import docking.widgets.EventTrigger;
import docking.widgets.SearchLocation;
import docking.widgets.fieldpanel.FieldPanel;
import docking.widgets.fieldpanel.LayoutModel;
import docking.widgets.fieldpanel.field.Field;
@@ -44,6 +43,7 @@ import ghidra.app.decompiler.component.margin.*;
import ghidra.app.decompiler.location.*;
import ghidra.app.plugin.core.decompile.DecompilerClipboardProvider;
import ghidra.app.plugin.core.decompile.actions.DecompilerSearchLocation;
import ghidra.app.plugin.core.decompile.actions.DecompilerSearchResults;
import ghidra.app.util.viewer.util.ScrollpaneAlignedHorizontalLayout;
import ghidra.program.model.address.*;
import ghidra.program.model.listing.*;
@@ -95,8 +95,11 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field
private int middleMouseHighlightButton;
private Color middleMouseHighlightColor;
private Color currentVariableHighlightColor;
private Color activeSearchHighlightColor;
private Color searchHighlightColor;
private SearchLocation currentSearchLocation;
private DecompilerSearchResults currentSearchResults;
private DecompileData decompileData = new EmptyDecompileData("No Function");
private final DecompilerClipboardProvider clipboard;
@@ -145,6 +148,7 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field
decompilerHoverProvider = new DecompilerHoverProvider();
activeSearchHighlightColor = options.getActiveSearchHighlightColor();
searchHighlightColor = options.getSearchHighlightColor();
currentVariableHighlightColor = options.getCurrentVariableHighlightColor();
middleMouseHighlightColor = options.getMiddleMouseHighlightColor();
@@ -535,7 +539,10 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field
}
// don't highlight search results across functions
currentSearchLocation = null;
if (currentSearchResults != null) {
currentSearchResults.decompilerUpdated();
currentSearchResults = null;
}
if (function != null) {
highlightController.reapplyAllHighlights(function);
@@ -1105,13 +1112,32 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field
return null;
}
public void setSearchResults(SearchLocation searchLocation) {
currentSearchLocation = searchLocation;
public void clearSearchResults(DecompilerSearchResults searchResults) {
if (currentSearchResults == searchResults) {
currentSearchResults = null;
repaint();
}
}
public void setSearchResults(DecompilerSearchResults searchResults) {
currentSearchResults = searchResults;
if (currentSearchResults != null) {
DecompilerSearchLocation location = currentSearchResults.getActiveLocation();
if (location != null) {
setCursorPosition(location.getFieldLocation());
}
}
repaint();
}
public DecompilerSearchLocation getSearchResults() {
return (DecompilerSearchLocation) currentSearchLocation;
public DecompilerSearchLocation getActiveSearchLocation() {
if (currentSearchResults == null) {
return null;
}
DecompilerSearchLocation location = currentSearchResults.getActiveLocation();
return location;
}
public Color getCurrentVariableHighlightColor() {
@@ -1370,23 +1396,30 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field
@Override
public Highlight[] createHighlights(Field field, String text, int cursorTextOffset) {
if (currentSearchLocation == null) {
if (currentSearchResults == null) {
return new Highlight[0];
}
ClangTextField cField = (ClangTextField) field;
int highlightLine = cField.getLineNumber();
FieldLocation searchCursorLocation =
((DecompilerSearchLocation) currentSearchLocation).getFieldLocation();
int searchLineNumber = searchCursorLocation.getIndex().intValue() + 1;
if (highlightLine != searchLineNumber) {
// only highlight the match on the actual line
int lineNumber = cField.getLineNumber();
Map<Integer, List<DecompilerSearchLocation>> locationsByLine =
currentSearchResults.getLocationsByLine();
List<DecompilerSearchLocation> locationsOnLine = locationsByLine.get(lineNumber);
if (locationsOnLine.isEmpty()) {
return new Highlight[0];
}
return new Highlight[] { new Highlight(currentSearchLocation.getStartIndexInclusive(),
currentSearchLocation.getEndIndexInclusive(), searchHighlightColor) };
DecompilerSearchLocation activeLocation = currentSearchResults.getActiveLocation();
List<Highlight> highlights = new ArrayList<>();
for (DecompilerSearchLocation location : locationsOnLine) {
Color c =
location == activeLocation ? activeSearchHighlightColor : searchHighlightColor;
int start = location.getStartIndexInclusive();
int end = location.getEndIndexInclusive();
highlights.add(new Highlight(start, end, c));
}
return highlights.toArray(Highlight[]::new);
}
}

View File

@@ -1,13 +1,12 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.
@@ -35,4 +34,9 @@ public class DecompilerCursorPosition extends CursorPosition {
public void setOffset(int offset) {
location.col += offset;
}
@Override
public String toString() {
return location.toString();
}
}

View File

@@ -18,22 +18,20 @@ package ghidra.app.plugin.core.decompile.actions;
import docking.widgets.CursorPosition;
import docking.widgets.SearchLocation;
import docking.widgets.fieldpanel.support.FieldLocation;
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext;
import docking.widgets.search.SearchLocationContext;
public class DecompilerSearchLocation extends SearchLocation {
private final FieldLocation fieldLocation;
private String textLine;
private LocationReferenceContext context;
public DecompilerSearchLocation(FieldLocation fieldLocation, int startIndexInclusive,
int endIndexInclusive, String searchText, boolean forwardDirection, String textLine,
LocationReferenceContext context) {
int endIndexInclusive, String text, boolean forwardDirection, String textLine,
int lineNumber, SearchLocationContext context) {
super(startIndexInclusive, endIndexInclusive, searchText, forwardDirection);
super(startIndexInclusive, endIndexInclusive, text, lineNumber, context);
this.fieldLocation = fieldLocation;
this.textLine = textLine;
this.context = context;
}
public FieldLocation getFieldLocation() {
@@ -44,10 +42,6 @@ public class DecompilerSearchLocation extends SearchLocation {
return textLine;
}
public LocationReferenceContext getContext() {
return context;
}
@Override
public CursorPosition getCursorPosition() {
return new DecompilerCursorPosition(fieldLocation);
@@ -57,4 +51,15 @@ public class DecompilerSearchLocation extends SearchLocation {
protected String fieldsToString() {
return super.fieldsToString() + ", fieldLocation=" + fieldLocation;
}
public boolean contains(FieldLocation other) {
int line = getLineNumber();
int otherLine = other.getIndex().intValue() + 1; // +1 for zero based
if (line != otherLine) {
return false;
}
int col = other.getCol();
return contains(col);
}
}

View File

@@ -0,0 +1,245 @@
/* ###
* 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.plugin.core.decompile.actions;
import java.time.Duration;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import docking.widgets.SearchLocation;
import docking.widgets.fieldpanel.support.FieldLocation;
import docking.widgets.search.SearchResults;
import ghidra.app.decompiler.component.DecompilerController;
import ghidra.app.decompiler.component.DecompilerPanel;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.Program;
import ghidra.program.util.ProgramLocation;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
import ghidra.util.worker.Worker;
public class DecompilerSearchResults extends SearchResults {
// the location when the search was performed; used to know when the function has changed
private ProgramLocation programLocation;
private DecompilerPanel decompilerPanel;
private String searchText;
private List<SearchLocation> searchLocations;
private Map<Integer, List<DecompilerSearchLocation>> locationsByLine;
private TreeMap<LinePosition, DecompilerSearchLocation> matchesByPosition = new TreeMap<>();
private DecompilerSearchLocation activeLocation;
DecompilerSearchResults(Worker worker, DecompilerPanel decompilerPanel, String searchText,
List<SearchLocation> searchLocations) {
super(worker);
this.decompilerPanel = decompilerPanel;
this.searchText = searchText;
this.searchLocations = searchLocations;
this.programLocation = decompilerPanel.getCurrentLocation();
for (SearchLocation location : searchLocations) {
int line = location.getLineNumber();
int col = location.getStartIndexInclusive();
LinePosition lp = new LinePosition(line, col);
DecompilerSearchLocation dsl = (DecompilerSearchLocation) location;
matchesByPosition.put(lp, dsl);
}
}
@Override
public String getName() {
DecompilerController controller = decompilerPanel.getController();
Function function = controller.getFunction();
return function.getName() + "()";
}
ProgramLocation getDecompileLocation() {
return programLocation;
}
boolean isInvalid(String otherSearchText) {
if (isDifferentFunction()) {
return true;
}
return !searchText.equals(otherSearchText);
}
@Override
public boolean isEmpty() {
return searchLocations.isEmpty();
}
@Override
public List<SearchLocation> getLocations() {
return searchLocations;
}
public Map<Integer, List<DecompilerSearchLocation>> getLocationsByLine() {
if (locationsByLine == null) {
locationsByLine = searchLocations.stream()
.map(l -> (DecompilerSearchLocation) l)
.collect(Collectors.groupingBy(l -> l.getLineNumber()));
}
return locationsByLine;
}
private boolean isDifferentFunction() {
return !decompilerPanel.containsLocation(programLocation);
}
private boolean isMyFunction() {
return decompilerPanel.containsLocation(programLocation);
}
public DecompilerSearchLocation getContainingLocation(FieldLocation fieldLocation,
boolean searchForward) {
// getNextLocation() will find the next matching location, starting at the given field
// location. The next location may or may not actually contain the given field location.
DecompilerSearchLocation nextLocation = getNextLocation(fieldLocation, searchForward);
if (nextLocation.contains(fieldLocation)) {
return nextLocation;
}
return null;
}
@Override
public DecompilerSearchLocation getActiveLocation() {
return activeLocation;
}
private void installSearchResults() {
if (isDifferentFunction()) {
return; // a different function was decompiled while we were running
}
decompilerPanel.setSearchResults(this);
}
private void clearSearchResults() {
decompilerPanel.clearSearchResults(this);
}
public void decompilerUpdated() {
// The decompiler has updated. It may have been upon our request. If not, deactivate.
if (isDifferentFunction()) {
deactivate();
}
}
@Override
public void deactivate() {
FindJob job = new SwingJob(this::clearSearchResults);
runJob(job);
}
@Override
public void activate() {
FindJob job = createActivationJob().thenRunSwing(this::installSearchResults);
runJob(job);
}
@Override
public void setActiveLocation(SearchLocation location) {
if (activeLocation == location) {
return;
}
activeLocation = (DecompilerSearchLocation) location;
if (location == null) {
return;
}
// activate() will set the active search location
activate();
}
private ActivationJob createActivationJob() {
if (isMyFunction()) {
return createFinishedActivationJob(); // nothing to do
}
return (ActivationJob) new ActivateFunctionJob()
.thenWait(this::isMyFunction, Duration.ofSeconds(5));
}
protected ActivationJob createFinishedActivationJob() {
return new ActivationJob();
}
@Override
public void dispose() {
setActiveLocation(null);
decompilerPanel.clearSearchResults(this);
searchLocations.clear();
}
DecompilerSearchLocation getNextLocation(FieldLocation startLocation,
boolean searchForward) {
Entry<LinePosition, DecompilerSearchLocation> entry;
int line = startLocation.getIndex().intValue() + 1; // +1 for zero based
int col = startLocation.getCol();
LinePosition lp = new LinePosition(line, col);
if (searchForward) {
entry = matchesByPosition.ceilingEntry(lp);
}
else {
entry = matchesByPosition.floorEntry(lp);
}
if (entry == null) {
return null; // no more matches in the current direction
}
return entry.getValue();
}
//=================================================================================================
// Inner Classes
//=================================================================================================
private class ActivateFunctionJob extends ActivationJob {
@Override
protected void doRun(TaskMonitor monitor) throws CancelledException {
if (isMyFunction()) {
return; // nothing to do
}
DecompilerController controller = decompilerPanel.getController();
Program program = programLocation.getProgram();
controller.refreshDisplay(program, programLocation, null);
}
}
private record LinePosition(int line, int col) implements Comparable<LinePosition> {
@Override
public int compareTo(LinePosition other) {
int result = line - other.line;
if (result != 0) {
return result;
}
return col - other.col;
}
}
}

View File

@@ -15,27 +15,31 @@
*/
package ghidra.app.plugin.core.decompile.actions;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.regex.*;
import docking.widgets.*;
import docking.widgets.CursorPosition;
import docking.widgets.SearchLocation;
import docking.widgets.fieldpanel.field.Field;
import docking.widgets.fieldpanel.support.FieldLocation;
import docking.widgets.fieldpanel.support.RowColLocation;
import docking.widgets.search.*;
import ghidra.app.decompiler.component.ClangTextField;
import ghidra.app.decompiler.component.DecompilerPanel;
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext;
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContextBuilder;
import ghidra.util.Msg;
import ghidra.util.UserSearchUtils;
import ghidra.util.worker.Worker;
/**
* A {@link FindDialogSearcher} for searching the text of the decompiler window.
*/
public class DecompilerSearcher implements FindDialogSearcher {
private Worker worker = Worker.createGuiWorker();
private DecompilerPanel decompilerPanel;
private DecompilerSearchResults searchResults;
/**
* Constructor
@@ -76,98 +80,39 @@ public class DecompilerSearcher implements FindDialogSearcher {
return new DecompilerCursorPosition(fieldLocation);
}
@Override
public void setCursorPosition(CursorPosition position) {
decompilerPanel.setCursorPosition(((DecompilerCursorPosition) position).getFieldLocation());
}
@Override
public void highlightSearchResults(SearchLocation location) {
decompilerPanel.setSearchResults(location);
}
@Override
public void clearHighlights() {
decompilerPanel.setSearchResults(null);
}
@Override
public void dispose() {
clearHighlights();
decompilerPanel.setSearchResults(null);
if (searchResults != null) {
searchResults.dispose();
}
}
@Override
public SearchLocation search(String text, CursorPosition position, boolean searchForward,
boolean useRegex) {
DecompilerCursorPosition decompilerCursorPosition = (DecompilerCursorPosition) position;
FieldLocation startLocation =
getNextSearchStartLocation(decompilerCursorPosition, searchForward);
return doFind(text, startLocation, searchForward, useRegex);
}
private void updateSearchResults(String text, boolean useRegex) {
if (searchResults != null) {
if (!searchResults.isInvalid(text)) {
private FieldLocation getNextSearchStartLocation(
DecompilerCursorPosition decompilerCursorPosition, boolean searchForward) {
// the current results are still valid; ensure the highlights are still active
searchResults.activate();
return;
}
FieldLocation startLocation = decompilerCursorPosition.getFieldLocation();
DecompilerSearchLocation currentSearchLocation = decompilerPanel.getSearchResults();
if (currentSearchLocation == null) {
return startLocation; // nothing to do; no prior search hit
searchResults.dispose();
searchResults = null;
}
//
// Special Case Handling: Start the search at the cursor location by default.
// However, if the cursor location is at the beginning of previous search hit, then
// move the cursor forward by one character to ensure the previous search hit is not
// found.
//
// Note: for a forward or backward search the cursor is placed at the beginning of the
// match.
//
if (Objects.equals(startLocation, currentSearchLocation.getFieldLocation())) {
if (searchForward) {
// Given:
// -search text: 'fox'
// -search domain: 'What the |fox say'
// -a previous search hit just before 'fox'
//
// Move the cursor just past the 'f' so the next forward search will not
// find the current 'fox' hit. Thus the new search domain for this line
// will be: "ox say"
//
startLocation.col += 1;
}
else {
// Given:
// -search text: 'fox'
// -search domain: 'What the |fox say'
// -a previous search hit just before 'fox'
//
// Move the cursor just past the 'o' so the next backward search will not
// find the current 'fox' hit. Thus the new search domain for this line
// will be: "What the fo"
//
int length = currentSearchLocation.getMatchLength();
startLocation.col += length - 1;
}
}
return startLocation;
searchResults = doSearch(text, useRegex);
}
//=================================================================================================
// Search Methods
//=================================================================================================
private DecompilerSearchResults doSearch(String searchText, boolean isRegex) {
@Override
public List<SearchLocation> searchAll(String searchString, boolean isRegex) {
Pattern pattern = createPattern(searchString, isRegex);
Function<String, SearchMatch> function = createForwardMatchFunction(pattern);
Pattern pattern = createPattern(searchText, isRegex);
Function<String, SearchMatch> forwardMatcher = createForwardMatchFunction(pattern);
FieldLocation start = new FieldLocation();
List<SearchLocation> results = new ArrayList<>();
DecompilerSearchLocation searchLocation = findNext(function, searchString, start);
DecompilerSearchLocation searchLocation = findNext(forwardMatcher, searchText, start);
while (searchLocation != null) {
results.add(searchLocation);
@@ -177,24 +122,62 @@ public class DecompilerSearcher implements FindDialogSearcher {
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);
searchLocation = findNext(forwardMatcher, searchText, start);
}
return results;
DecompilerSearchResults newResults =
new DecompilerSearchResults(worker, decompilerPanel, searchText, results);
newResults.activate();
return newResults;
}
private DecompilerSearchLocation doFind(String searchString, FieldLocation currentLocation,
boolean forwardSearch, boolean isRegex) {
//=================================================================================================
// Search Methods
//=================================================================================================
Pattern pattern = createPattern(searchString, isRegex);
@Override
public SearchResults search(String text, CursorPosition position, boolean searchForward,
boolean useRegex) {
if (forwardSearch) {
Function<String, SearchMatch> function = createForwardMatchFunction(pattern);
return findNext(function, searchString, currentLocation);
updateSearchResults(text, useRegex);
DecompilerCursorPosition cursorPosition = (DecompilerCursorPosition) position;
FieldLocation startLocation = getNextSearchStartLocation(cursorPosition, searchForward);
DecompilerSearchLocation location =
searchResults.getNextLocation(startLocation, searchForward);
if (location == null) {
return null;
}
Function<String, SearchMatch> reverse = createReverseMatchFunction(pattern);
return findPrevious(reverse, searchString, currentLocation);
searchResults.setActiveLocation(location);
return searchResults;
}
private FieldLocation getNextSearchStartLocation(
DecompilerCursorPosition decompilerCursorPosition, boolean searchForward) {
FieldLocation cursor = decompilerCursorPosition.getFieldLocation();
DecompilerSearchLocation containingLocation =
searchResults.getContainingLocation(cursor, searchForward);
if (containingLocation == null) {
return cursor; // nothing to do; not on a search hit
}
// the given cursor position is inside of an existing match
if (searchForward) {
cursor.col += 1;
}
else {
cursor.col = containingLocation.getStartIndexInclusive() - 1;
}
return cursor;
}
@Override
public SearchResults searchAll(String searchString, boolean isRegex) {
return doSearch(searchString, isRegex);
}
private Pattern createPattern(String searchString, boolean isRegex) {
@@ -230,35 +213,6 @@ public class DecompilerSearcher implements FindDialogSearcher {
}
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) {
@@ -288,15 +242,16 @@ public class DecompilerSearcher implements FindDialogSearcher {
FieldLineLocation lineInfo = getFieldIndexFromOffset(match.start, field);
FieldLocation fieldLocation =
new FieldLocation(i, lineInfo.fieldNumber(), 0, lineInfo.column());
LocationReferenceContext context = createContext(fullLine, match);
int lineNumber = lineInfo.lineNumber();
SearchLocationContext context = createContext(fullLine, match);
return new DecompilerSearchLocation(fieldLocation, match.start, match.end - 1,
searchString, true, field.getText(), context);
searchString, true, field.getText(), lineNumber, context);
}
return null;
}
private LocationReferenceContext createContext(String line, SearchMatch match) {
LocationReferenceContextBuilder builder = new LocationReferenceContextBuilder();
private SearchLocationContext createContext(String line, SearchMatch match) {
SearchLocationContextBuilder builder = new SearchLocationContextBuilder();
int start = match.start;
int end = match.end;
builder.append(line.substring(0, start));
@@ -308,29 +263,6 @@ public class DecompilerSearcher implements FindDialogSearcher {
return builder.build();
}
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) {
continue;
}
FieldLineLocation lineInfo = getFieldIndexFromOffset(match.start, field);
FieldLocation fieldLocation =
new FieldLocation(i, lineInfo.fieldNumber(), 0, lineInfo.column());
LocationReferenceContext context = createContext(field.getText(), match);
return new DecompilerSearchLocation(fieldLocation, match.start, match.end - 1,
searchString, false, field.getText(), context);
}
return null;
}
private String substring(ClangTextField textField, FieldLocation location,
boolean forwardSearch) {
@@ -364,9 +296,10 @@ public class DecompilerSearcher implements FindDialogSearcher {
private FieldLineLocation getFieldIndexFromOffset(int screenOffset, ClangTextField textField) {
RowColLocation rowColLocation = textField.textOffsetToScreenLocation(screenOffset);
int lineNumber = textField.getLineNumber();
// we use 0 here because currently there is only one field, which is the entire line
return new FieldLineLocation(0, rowColLocation.col());
return new FieldLineLocation(0, lineNumber, rowColLocation.col());
}
private static class SearchMatch {
@@ -390,5 +323,5 @@ public class DecompilerSearcher implements FindDialogSearcher {
}
}
private record FieldLineLocation(int fieldNumber, int column) {}
private record FieldLineLocation(int fieldNumber, int lineNumber, int column) {}
}

View File

@@ -81,6 +81,11 @@ public class FindAction extends AbstractDecompilerAction {
dialog.setSearchText(text);
}
if (dialog.isShowing()) {
dialog.toFront();
return;
}
// show over the root frame, so the user can still see the Decompiler window
context.getTool().showDialog(dialog);
}

View File

@@ -4,9 +4,9 @@
* 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.
@@ -340,7 +340,7 @@ public class DecompilerFindDialogTest extends AbstractDecompilerTest {
assertCurrentLocation(line, column);
DecompilerPanel panel = getDecompilerPanel();
DecompilerSearchLocation searchResults = panel.getSearchResults();
DecompilerSearchLocation searchResults = panel.getActiveSearchLocation();
FieldLocation searchCursorLocation = searchResults.getFieldLocation();
int searchLineNumber = searchCursorLocation.getIndex().intValue() + 1;
assertEquals("Search result is on the wrong line", line, searchLineNumber);

View File

@@ -17,9 +17,9 @@ package ghidra.app.extension.datatype.finder;
import java.util.List;
import docking.widgets.search.SearchLocationContext;
import docking.widgets.search.SearchLocationContextBuilder;
import ghidra.app.decompiler.*;
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext;
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContextBuilder;
import ghidra.app.services.DataTypeReference;
import ghidra.app.services.FieldMatcher;
import ghidra.program.model.address.Address;
@@ -87,14 +87,14 @@ public abstract class DecompilerReference {
return line;
}
protected LocationReferenceContext getContext() {
LocationReferenceContext context = getContext(variable);
protected SearchLocationContext getContext() {
SearchLocationContext context = getContext(variable);
return context;
}
protected LocationReferenceContext getContext(DecompilerVariable var) {
protected SearchLocationContext getContext(DecompilerVariable var) {
LocationReferenceContextBuilder builder = new LocationReferenceContextBuilder();
SearchLocationContextBuilder builder = new SearchLocationContextBuilder();
builder.append(line.getLineNumber() + ": ");
List<ClangToken> tokens = line.getAllTokens();
for (ClangToken token : tokens) {

View File

@@ -18,8 +18,8 @@ package ghidra.app.extension.datatype.finder;
import java.util.ArrayList;
import java.util.List;
import docking.widgets.search.SearchLocationContext;
import ghidra.app.decompiler.*;
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext;
import ghidra.app.services.DataTypeReference;
import ghidra.app.services.FieldMatcher;
import ghidra.program.model.address.Address;
@@ -240,7 +240,7 @@ public class VariableAccessDR extends DecompilerReference {
protected DataTypeReference createReference(DecompilerVariable var) {
DataType dataType = var.getDataType();
LocationReferenceContext context = getContext(var);
SearchLocationContext context = getContext(var);
Function function = var.getFunction();
Address address = getAddress(var);
return new DataTypeReference(dataType, null, function, address, context);
@@ -248,16 +248,16 @@ public class VariableAccessDR extends DecompilerReference {
private DataTypeReference createReference(DecompilerVariable var, DecompilerVariable field) {
DataType dataType = var.getDataType();
LocationReferenceContext context = getContext(var);
SearchLocationContext context = getContext(var);
Function function = var.getFunction();
Address address = getAddress(var);
return new DataTypeReference(dataType, field.getName(), function, address, context);
}
@Override
protected LocationReferenceContext getContext(DecompilerVariable var) {
protected SearchLocationContext getContext(DecompilerVariable var) {
DecompilerVariable field = findFieldFor(var);
LocationReferenceContext context = super.getContext(field);
SearchLocationContext context = super.getContext(field);
return context;
}

View File

@@ -4,9 +4,9 @@
* 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.
@@ -17,8 +17,8 @@ package ghidra.app.extension.datatype.finder;
import java.util.List;
import docking.widgets.search.SearchLocationContext;
import ghidra.app.decompiler.*;
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext;
import ghidra.app.services.DataTypeReference;
import ghidra.app.services.FieldMatcher;
import ghidra.program.model.address.Address;
@@ -74,7 +74,7 @@ public abstract class VariableDR extends DecompilerReference {
String fieldName = fieldMatcher.getFieldName();
Function function = getFunction();
Address address = getAddress();
LocationReferenceContext context = getContext();
SearchLocationContext context = getContext();
results.add(new DataTypeReference(dataType, fieldName, function, address, context));
}
}

View File

@@ -20,12 +20,12 @@ import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import docking.widgets.search.SearchLocationContext;
import docking.widgets.search.SearchLocationContextBuilder;
import generic.json.Json;
import ghidra.app.decompiler.*;
import ghidra.app.decompiler.component.DecompilerUtils;
import ghidra.app.decompiler.parallel.*;
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext;
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContextBuilder;
import ghidra.program.model.address.AddressSet;
import ghidra.program.model.address.AddressSpace;
import ghidra.program.model.listing.*;
@@ -246,15 +246,15 @@ public class DecompilerTextFinder {
TextLine firstLine = lineMatches.get(0);
int lineNumber = firstLine.getLineNumber();
AddressSet addresses = getAddresses(function, firstLine.getCLine());
LocationReferenceContext context = createMatchContext(lineMatches);
SearchLocationContext context = createMatchContext(lineMatches);
TextMatch match =
new TextMatch(function, addresses, lineNumber, searchText, context, true);
callback.accept(match);
}
private LocationReferenceContext createMatchContext(List<TextLine> matches) {
private SearchLocationContext createMatchContext(List<TextLine> matches) {
LocationReferenceContextBuilder builder = new LocationReferenceContextBuilder();
SearchLocationContextBuilder builder = new SearchLocationContextBuilder();
for (TextLine line : matches) {
if (!builder.isEmpty()) {
builder.newline();
@@ -279,7 +279,7 @@ public class DecompilerTextFinder {
return;
}
LocationReferenceContextBuilder builder = new LocationReferenceContextBuilder();
SearchLocationContextBuilder builder = new SearchLocationContextBuilder();
int start = matcher.start();
int end = matcher.end();
@@ -291,7 +291,7 @@ public class DecompilerTextFinder {
int lineNumber = line.getLineNumber();
AddressSet addresses = getAddresses(function, line);
LocationReferenceContext context = builder.build();
SearchLocationContext context = builder.build();
TextMatch match =
new TextMatch(function, addresses, lineNumber, searchText, context, false);
callback.accept(match);

View File

@@ -20,8 +20,8 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import docking.widgets.search.SearchLocationContext;
import docking.widgets.table.*;
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext;
import ghidra.docking.settings.Settings;
import ghidra.framework.plugintool.ServiceProvider;
import ghidra.program.model.address.Address;
@@ -142,12 +142,12 @@ public class DecompilerTextFinderTableModel extends GhidraProgramTableModel<Text
}
private class ContextTableColumn
extends AbstractProgramBasedDynamicTableColumn<TextMatch, LocationReferenceContext> {
extends AbstractProgramBasedDynamicTableColumn<TextMatch, SearchLocationContext> {
private ContextCellRenderer renderer = new ContextCellRenderer();
@Override
public LocationReferenceContext getValue(TextMatch rowObject, Settings settings, Program p,
public SearchLocationContext getValue(TextMatch rowObject, Settings settings, Program p,
ServiceProvider sp) throws IllegalArgumentException {
return rowObject.getContext();
}
@@ -158,13 +158,13 @@ public class DecompilerTextFinderTableModel extends GhidraProgramTableModel<Text
}
@Override
public GColumnRenderer<LocationReferenceContext> getColumnRenderer() {
public GColumnRenderer<SearchLocationContext> getColumnRenderer() {
return renderer;
}
}
private class ContextCellRenderer
extends AbstractGhidraColumnRenderer<LocationReferenceContext> {
extends AbstractGhidraColumnRenderer<SearchLocationContext> {
{
// the context uses html
@@ -178,7 +178,7 @@ public class DecompilerTextFinderTableModel extends GhidraProgramTableModel<Text
super.getTableCellRendererComponent(data);
TextMatch match = (TextMatch) data.getRowObject();
LocationReferenceContext context = match.getContext();
SearchLocationContext context = match.getContext();
String text;
if (match.isMultiLine()) {
// multi-line matches create visual noise when showing colors, as of much of the
@@ -193,7 +193,7 @@ public class DecompilerTextFinderTableModel extends GhidraProgramTableModel<Text
}
@Override
public String getFilterString(LocationReferenceContext context, Settings settings) {
public String getFilterString(SearchLocationContext context, Settings settings) {
return context.getPlainText();
}
}

View File

@@ -15,8 +15,8 @@
*/
package ghidra.app.plugin.core.search;
import docking.widgets.search.SearchLocationContext;
import generic.json.Json;
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressSet;
import ghidra.program.model.listing.Function;
@@ -28,14 +28,14 @@ public class TextMatch {
private Function function;
private AddressSet addresses;
private LocationReferenceContext context;
private SearchLocationContext context;
private int lineNumber;
private String searchText;
private boolean isMultiLine;
TextMatch(Function function, AddressSet addresses, int lineNumber, String searchText,
LocationReferenceContext context, boolean isMultiLine) {
SearchLocationContext context, boolean isMultiLine) {
this.function = function;
this.addresses = addresses;
this.lineNumber = lineNumber;
@@ -48,7 +48,7 @@ public class TextMatch {
return function;
}
public LocationReferenceContext getContext() {
public SearchLocationContext getContext() {
return context;
}

View File

@@ -106,6 +106,7 @@ src/main/resources/images/play.png||GHIDRA||||END|
src/main/resources/images/preferences-system-windows.png||Tango Icons - Public Domain||||END|
src/main/resources/images/software-update-available.png||Tango Icons - Public Domain|||tango icon set|END|
src/main/resources/images/table.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
src/main/resources/images/table_delete.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
src/main/resources/images/table_relationship.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
src/main/resources/images/tag.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
src/main/resources/images/text_lowercase.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|

View File

@@ -130,6 +130,7 @@ icon.widget.pathmanager.reset = trash-empty.png
icon.widget.table.header.help = info_small.png
icon.widget.table.header.help.hovered = info_small_hover.png
icon.widget.table.header.pending = icon.pending
icon.widget.table.delete.row = table_delete.png
icon.widget.tabs.empty.small = empty8x16.png
icon.widget.tabs.close = x.gif

View File

@@ -318,14 +318,16 @@ class ComponentNode extends Node {
ComponentPlaceholder placeholder = activeComponents.get(i);
DockableComponent c = placeholder.getComponent();
c.setBorder(BorderFactory.createEmptyBorder());
String title = placeholder.getTitle();
// The renderer uses use the full title as the tooltip for the tab
String fullTitle = placeholder.getFullTitle();
String tabText = placeholder.getTabText();
final DockableComponent component = placeholder.getComponent();
tabbedPane.add(component, title);
DockableComponent component = placeholder.getComponent();
tabbedPane.add(fullTitle, component);
DockingTabRenderer tabRenderer =
createTabRenderer(tabbedPane, placeholder, title, tabText, component);
createTabRenderer(tabbedPane, placeholder, fullTitle, tabText, component);
c.installDragDropTarget(tabbedPane);

View File

@@ -448,6 +448,7 @@ public class ComponentPlaceholder {
* @param newProvider the new provider
*/
void setProvider(ComponentProvider newProvider) {
this.componentProvider = newProvider;
actions.clear();
if (newProvider != null) {

View File

@@ -923,7 +923,7 @@ public abstract class ComponentProvider implements HelpDescriptor, ActionContext
*
* @param group the group for this provider.
*/
protected void setWindowGroup(String group) {
public void setWindowGroup(String group) {
this.group = group;
}

View File

@@ -18,28 +18,33 @@ package docking.help;
import java.awt.Component;
import java.awt.Window;
import java.awt.event.*;
import java.io.File;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.net.URL;
import java.util.Enumeration;
import java.time.Duration;
import java.util.*;
import javax.help.*;
import javax.help.search.SearchEngine;
import javax.swing.*;
import javax.swing.text.Document;
import docking.DockingUtils;
import docking.DockingWindowManager;
import docking.actions.KeyBindingUtils;
import docking.widgets.FindDialog;
import docking.widgets.TextComponentSearcher;
import docking.widgets.SearchLocation;
import docking.widgets.search.*;
import generic.util.WindowUtilities;
import ghidra.util.exception.AssertException;
import ghidra.util.task.TaskMonitor;
import ghidra.util.worker.Worker;
/**
* Enables the Find Dialog for searching through the current page of a help document.
*/
class HelpViewSearcher {
private static final String DIALOG_TITLE_PREFIX = "Whole Word Search in ";
private static final String FIND_ACTION_NAME = "find.action";
private static KeyStroke FIND_KEYSTROKE =
@@ -59,24 +64,11 @@ class HelpViewSearcher {
JHelpContentViewer contentViewer = jHelp.getContentViewer();
contentViewer.addHelpModelListener(e -> {
URL url = e.getURL();
if (!isValidHelpURL(url)) {
// invalid file--don't enable searching for it
return;
}
String file = url.getFile();
int separatorIndex = file.lastIndexOf(File.separator);
file = file.substring(separatorIndex + 1);
findDialog.setTitle(DIALOG_TITLE_PREFIX + file);
});
// note: see HTMLEditorKit$LinkController.mouseMoved() for inspiration
htmlEditorPane = getHTMLEditorPane(contentViewer);
TextComponentSearcher searcher = new TextComponentSearcher(htmlEditorPane);
findDialog = new FindDialog(DIALOG_TITLE_PREFIX, searcher);
HtmlTextSearcher searcher = new HtmlTextSearcher(htmlEditorPane);
findDialog = new FindDialog("Help Find", searcher);
htmlEditorPane.addMouseListener(new MouseAdapter() {
@Override
@@ -93,14 +85,6 @@ class HelpViewSearcher {
// highlighter.addHighlight(0, 0, null)
}
private boolean isValidHelpURL(URL url) {
if (url == null) {
return false;
}
String file = url.getFile();
return new File(file).exists();
}
private void grabSearchEngine() {
Enumeration<?> navigators = jHelp.getHelpNavigators();
while (navigators.hasMoreElements()) {
@@ -193,6 +177,156 @@ class HelpViewSearcher {
}
}
private class HtmlTextSearcher extends TextComponentSearcher {
public HtmlTextSearcher(JEditorPane editorPane) {
super(editorPane);
}
@Override
protected HtmlSearchResults createSearchResults(
Worker worker, JEditorPane jEditorPane, String searchText,
TreeMap<Integer, TextComponentSearchLocation> matchesByPosition) {
HtmlSearchResults results = new HtmlSearchResults(worker, jEditorPane, searchText,
matchesByPosition);
TextHelpModel model = jHelp.getModel();
URL url = model.getCurrentURL();
results.setUrl(url);
return results;
}
}
private class HtmlSearchResults extends TextComponentSearchResults {
private PageLoadedListener pageLoadListener;
private URL searchUrl;
// we use the document length to know when our page is finished loading on a reload
private int fullDocumentLength;
private String name;
HtmlSearchResults(Worker worker, JEditorPane editorPane, String searchText,
TreeMap<Integer, TextComponentSearchLocation> matchesByPosition) {
super(worker, editorPane, searchText, matchesByPosition);
pageLoadListener = new PageLoadedListener(this);
editorPane.addPropertyChangeListener("page", pageLoadListener);
Document doc = editorPane.getDocument();
fullDocumentLength = doc.getLength();
}
@Override
public String getName() {
return name;
}
@Override
protected boolean isInvalid(String otherSearchText) {
if (!isMyHelpPageShowing()) {
return true;
}
return super.isInvalid(otherSearchText);
}
private boolean isMyHelpPageShowing() {
TextHelpModel model = jHelp.getModel();
URL htmlViewerURL = model.getCurrentURL();
if (!Objects.equals(htmlViewerURL, searchUrl)) {
// the help does not have my page
return false;
}
// The document gets loaded asynchronously. Use the length to know when it is finished
// loaded. This will not work correctly when the lengths are the same between two
// documents, but that should be a low occurrence event.
Document doc = editorPane.getDocument();
int currentLength = doc.getLength();
return fullDocumentLength == currentLength;
}
private void loadMyHelpPage(TaskMonitor m) {
if (isMyHelpPageShowing()) {
return; // no need to reload
}
// Trigger the URL of the results to load and then activate the results
jHelp.setCurrentURL(searchUrl);
}
/**
* Start an asynchronous activation. When we activate, we have to tell the viewer to load a
* new html page, which is asynchronous.
* @return the future
*/
@Override
protected ActivationJob createActivationJob() {
// start a new page load and then wait for it to finish
return (ActivationJob) super.createActivationJob()
.thenRun(this::loadMyHelpPage)
.thenWait(this::isMyHelpPageShowing, Duration.ofSeconds(3));
}
@Override
public void activate() {
//
// When we activate, a new page load may get triggered. When that happens the caret
// position will get moved by the help viewer. We will put back the last active search
// location after the load has finished.
//
SearchLocation lastActiveLocation = getActiveLocation();
FindJob job = startActivation()
.thenRunSwing(() -> restoreLocation(lastActiveLocation));
runActivationJob((ActivationJob) job);
}
private void restoreLocation(SearchLocation lastActiveLocation) {
if (lastActiveLocation != null) {
setActiveLocation(null);
setActiveLocation(lastActiveLocation);
}
}
@Override
public void dispose() {
editorPane.removePropertyChangeListener("page", pageLoadListener);
super.dispose();
}
void setUrl(URL url) {
searchUrl = url;
name = getFilename(searchUrl);
}
private URL getUrl() {
return searchUrl;
}
private class PageLoadedListener implements PropertyChangeListener {
private HtmlSearchResults htmlResults;
PageLoadedListener(HtmlSearchResults htmlResults) {
this.htmlResults = htmlResults;
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
URL newPage = (URL) evt.getNewValue();
if (!Objects.equals(newPage, htmlResults.getUrl())) {
htmlResults.deactivate();
}
}
}
}
//
// private class IndexerSearchTask extends Task {
//

View File

@@ -23,10 +23,14 @@ import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import docking.ReusableDialogComponentProvider;
import org.apache.commons.lang3.StringUtils;
import docking.*;
import docking.widgets.button.GRadioButton;
import docking.widgets.combobox.GhidraComboBox;
import docking.widgets.label.GLabel;
import docking.widgets.search.FindDialogSearcher;
import docking.widgets.search.SearchResults;
import utility.function.Callback;
/**
@@ -37,8 +41,12 @@ public class FindDialog extends ReusableDialogComponentProvider {
protected GhidraComboBox<String> comboBox;
protected FindDialogSearcher searcher;
protected SearchResults searchResults;
private JButton nextButton;
private JButton previousButton;
private JButton findAllButton;
private boolean isFindButtonApiDisabled;
private JRadioButton stringRadioButton;
private JRadioButton regexRadioButton;
@@ -49,7 +57,14 @@ public class FindDialog extends ReusableDialogComponentProvider {
this.searcher = searcher;
addWorkPanel(buildMainPanel());
buildButtons();
buildFindButtons();
addDismissButton();
}
public void setFindAllEnabled(boolean enabled) {
isFindButtonApiDisabled = !enabled;
findAllButton.setEnabled(enabled);
}
@Override
@@ -62,7 +77,7 @@ public class FindDialog extends ReusableDialogComponentProvider {
this.closedCallback = Callback.dummyIfNull(c);
}
private void buildButtons() {
protected void buildFindButtons() {
nextButton = new JButton("Next");
nextButton.setMnemonic('N');
nextButton.getAccessibleContext().setAccessibleName("Next");
@@ -76,7 +91,13 @@ public class FindDialog extends ReusableDialogComponentProvider {
previousButton.addActionListener(ev -> doSearch(false));
addButton(previousButton);
addDismissButton();
findAllButton = new JButton("Find All");
findAllButton.setMnemonic('A');
findAllButton.getAccessibleContext().setAccessibleName("Find All");
findAllButton.addActionListener(ev -> doSearchAll());
addButton(findAllButton);
enableButtons(false);
}
private JPanel buildMainPanel() {
@@ -137,6 +158,10 @@ public class FindDialog extends ReusableDialogComponentProvider {
protected void enableButtons(boolean b) {
nextButton.setEnabled(b);
previousButton.setEnabled(b);
if (!isFindButtonApiDisabled) {
findAllButton.setEnabled(b);
}
}
private JPanel buildFormatPanel() {
@@ -149,13 +174,6 @@ public class FindDialog extends ReusableDialogComponentProvider {
return formatPanel;
}
@Override
protected void dialogClosed() {
comboBox.setText("");
searcher.clearHighlights();
closedCallback.call();
}
public void next() {
doSearch(true);
}
@@ -168,7 +186,7 @@ public class FindDialog extends ReusableDialogComponentProvider {
return regexRadioButton.isSelected();
}
private void doSearch(boolean forward) {
protected void doSearch(boolean forward) {
if (!nextButton.isEnabled()) {
return; // don't search while disabled
@@ -179,14 +197,13 @@ public class FindDialog extends ReusableDialogComponentProvider {
String searchText = comboBox.getText();
CursorPosition cursorPosition = searcher.getCursorPosition();
SearchLocation searchLocation =
searcher.search(searchText, cursorPosition, forward, useRegex);
searchResults = searcher.search(searchText, cursorPosition, forward, useRegex);
//
// First, just search in the current direction.
//
if (searchLocation != null) {
notifySearchHit(searchLocation);
if (searchResults != null) {
storeSearchText(searchText);
return;
}
@@ -203,9 +220,9 @@ public class FindDialog extends ReusableDialogComponentProvider {
cursorPosition = searcher.getEnd();
}
searchLocation = searcher.search(searchText, cursorPosition, forward, useRegex);
if (searchLocation != null) {
notifySearchHit(searchLocation);
searchResults = searcher.search(searchText, cursorPosition, forward, useRegex);
if (searchResults != null) {
storeSearchText(searchText);
notifyUser(wrapMessage);
return;
}
@@ -218,12 +235,6 @@ public class FindDialog extends ReusableDialogComponentProvider {
notifyUser("Not found");
}
protected void notifySearchHit(SearchLocation location) {
searcher.setCursorPosition(location.getCursorPosition());
storeSearchText(location.getSearchText());
searcher.highlightSearchResults(location);
}
private void notifyUser(String message) {
setStatus(message);
@@ -236,11 +247,70 @@ public class FindDialog extends ReusableDialogComponentProvider {
});
}
protected void doSearchAll() {
DockingWindowManager dwm = DockingWindowManager.getActiveInstance();
if (dwm == null) {
return; // not sure this can happen
}
// Note: we do not save the SearchResults in this dialog. They will be managed by the
// provider we create below. This is in contrast to a single search, which will results.
// Further, when this method closes this dialog, the dialog's current results are cleared.
String searchText = getSearchText();
SearchResults results = searcher.searchAll(searchText, useRegex());
if (results.isEmpty()) {
setStatus("No results found");
return;
}
// save off searches that find results so users can reuse them later
storeSearchText(getSearchText());
String resultsName = results.getName();
if (StringUtils.isBlank(resultsName)) {
resultsName = "";
}
else {
resultsName = "[%s]".formatted(resultsName);
}
String dialogTitle = getTitle();
// e.g., Help Find: 'text' [Foo.html]
String subTitle = ": '%s' %s".formatted(searchText, resultsName);
Tool tool = dwm.getTool();
FindDialogResultsProvider provider =
new FindDialogResultsProvider(tool, dialogTitle, subTitle, results);
// set the tab text to the short and descriptive search term
provider.setTabText("'%s'".formatted(searchText));
close();
}
@Override
public void toFront() {
super.toFront();
String text = comboBox.getText();
enableButtons(text.length() != 0);
}
@Override
protected void dialogShown() {
clearStatusText();
}
@Override
protected void dialogClosed() {
comboBox.setText("");
if (searchResults != null) {
searchResults.dispose();
searchResults = null;
}
closedCallback.call();
}
public FindDialogSearcher getSearcher() {
return searcher;
}
@@ -286,4 +356,5 @@ public class FindDialog extends ReusableDialogComponentProvider {
// do this last since removing items may change the selected item
model.setSelectedItem(text);
}
}

View File

@@ -0,0 +1,342 @@
/* ###
* 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;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import javax.swing.*;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
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.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
implements TableModelListener {
private static final String OWNER_NAME = "Search";
private SearchResults searchResults;
private JPanel componentPanel;
private FindResultsModel model;
private GTable table;
private GTableFilterPanel<SearchLocation> filterPanel;
private DockingAction removeItemsAction;
FindDialogResultsProvider(Tool tool, String title, String subTitle,
SearchResults searchResults) {
super(tool, "Find All", OWNER_NAME);
this.searchResults = searchResults;
this.model = new FindResultsModel(searchResults);
setTransient();
setTitle(title + subTitle);
setSubTitle(subTitle);
setWindowMenuGroup(title);
componentPanel = buildMainPanel();
updateTitle();
addToTool();
installRemoveItemsAction();
model.addTableModelListener(this);
setVisible(true);
}
private JPanel buildMainPanel() {
JPanel panel = new JPanel(new BorderLayout());
table = new GTable(model);
table.setHTMLRenderingEnabled(true);
filterPanel = new GTableFilterPanel<>(table, model);
table.getSelectionModel().addListSelectionListener(e -> {
if (e.getValueIsAdjusting()) {
return;
}
getTool().contextChanged(FindDialogResultsProvider.this);
});
table.setActionsEnabled(true);
// add row listener to go to the field for that row when the user arrows
ListSelectionModel selectionModel = table.getSelectionModel();
selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
selectionModel.addListSelectionListener(lse -> {
if (lse.getValueIsAdjusting()) {
return;
}
setSearchLocationFromRow();
});
// this listener works around the case where the user clicks and already selected row
table.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() < 2) {
return;
}
setSearchLocationFromRow();
}
});
panel.add(new JScrollPane(table), BorderLayout.CENTER);
panel.add(filterPanel, BorderLayout.SOUTH);
return panel;
}
private void setSearchLocationFromRow() {
int row = table.getSelectedRow();
if (row == -1) {
searchResults.setActiveLocation(null);
return;
}
SearchLocation location = model.getRowObject(row);
searchResults.setActiveLocation(location);
}
public void installRemoveItemsAction() {
if (removeItemsAction != null) {
return;
}
removeItemsAction = new DeleteTableRowAction(table, OWNER_NAME) {
@Override
protected void removeSelectedItems() {
int[] rows = table.getSelectedRows();
List<Object> itemsToRemove = new ArrayList<>();
for (int row : rows) {
itemsToRemove.add(model.getRowObject(row));
}
removeRowObjects(model, itemsToRemove);
// put some selection back
int restoreRow = rows[0];
selectRow(model, restoreRow);
}
@Override
protected void removeRowObjects(TableModel tm, List<Object> itemsToRemove) {
model.remove(itemsToRemove);
}
};
getTool().addLocalAction(this, removeItemsAction);
}
private String generateSubTitle() {
StringBuilder buffer = new StringBuilder();
String filteredText = "";
if (filterPanel.isFiltered()) {
filteredText = " of " + filterPanel.getUnfilteredRowCount();
}
int n = model.getRowCount();
if (n == 1) {
buffer.append(" (1 entry").append(filteredText).append(")");
}
else if (n > 1) {
buffer.append(" (").append(n).append(" entries").append(filteredText).append(")");
}
return buffer.toString();
}
@Override
public void closeComponent() {
searchResults.dispose();
super.closeComponent();
filterPanel.dispose();
}
@Override
public JComponent getComponent() {
return componentPanel;
}
@Override
public void componentActivated() {
searchResults.activate();
setSearchLocationFromRow();
}
@Override
public void componentDeactived() {
// We don't want this, as the user may wish to click around in the text pane and keep the
// highlights, which this call would break.
// searchResults.deactivate();
}
@Override
public void tableChanged(TableModelEvent ev) {
updateTitle();
}
private void updateTitle() {
setSubTitle(generateSubTitle());
}
public GTable getTable() {
return table;
}
// for testing
public List<SearchLocation> getResults() {
return new ArrayList<>(model.getModelData());
}
private class FindResultsModel extends GDynamicColumnTableModel<SearchLocation, Object> {
private List<SearchLocation> data;
FindResultsModel(SearchResults results) {
super(new ServiceProviderStub());
this.data = results.getLocations();
}
void remove(List<Object> itemsToRemove) {
for (Object object : itemsToRemove) {
data.remove(object);
}
fireTableDataChanged();
}
@Override
public List<SearchLocation> getModelData() {
return data;
}
@Override
protected TableColumnDescriptor<SearchLocation> createTableColumnDescriptor() {
TableColumnDescriptor<SearchLocation> descriptor =
new TableColumnDescriptor<>();
descriptor.addVisibleColumn(new LineNumberColumn(), 1, true);
descriptor.addVisibleColumn(new ContextColumn());
return descriptor;
}
@Override
public String getName() {
return "Find All Results";
}
@Override
public Object getDataSource() {
return null;
}
private class LineNumberColumn
extends AbstractDynamicTableColumnStub<SearchLocation, Integer> {
@Override
public Integer getValue(SearchLocation rowObject, Settings settings,
ServiceProvider sp) throws IllegalArgumentException {
return rowObject.getLineNumber();
}
@Override
public String getColumnName() {
return "Line";
}
@Override
public int getColumnPreferredWidth() {
return 75;
}
}
private class ContextColumn extends
AbstractDynamicTableColumnStub<SearchLocation, SearchLocationContext> {
private GColumnRenderer<SearchLocationContext> renderer = new ContextCellRenderer();
@Override
public SearchLocationContext getValue(SearchLocation rowObject,
Settings settings,
ServiceProvider sp) throws IllegalArgumentException {
SearchLocationContext context = rowObject.getContext();
return context;
}
@Override
public String getColumnName() {
return "Context";
}
@Override
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();
}
}
}
}
}

View File

@@ -4,9 +4,9 @@
* 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.
@@ -15,32 +15,47 @@
*/
package docking.widgets;
import docking.widgets.search.SearchLocationContext;
/**
* An object that describes a search result.
*/
public class SearchLocation {
private final int startIndexInclusive;
private final int endIndexInclusive;
private final String searchText;
private final boolean forwardDirection;
private final String text;
private SearchLocationContext context;
private int lineNumber;
public SearchLocation(int startIndexInclusive, int endIndexInclusive, String searchText,
boolean forwardDirection) {
public SearchLocation(int startIndexInclusive, int endIndexInclusive, String text) {
this.startIndexInclusive = startIndexInclusive;
this.endIndexInclusive = endIndexInclusive;
this.searchText = searchText;
this.forwardDirection = forwardDirection;
this.text = text;
}
public SearchLocation(int startIndexInclusive, int endIndexInclusive, String text,
int lineNumber, SearchLocationContext context) {
this.startIndexInclusive = startIndexInclusive;
this.endIndexInclusive = endIndexInclusive;
this.text = text;
this.context = context;
this.lineNumber = lineNumber;
}
public SearchLocationContext getContext() {
return context;
}
public int getLineNumber() {
return lineNumber;
}
public CursorPosition getCursorPosition() {
return new CursorPosition(startIndexInclusive);
}
public String getSearchText() {
return searchText;
}
public int getEndIndexInclusive() {
return endIndexInclusive;
}
@@ -49,20 +64,21 @@ public class SearchLocation {
return startIndexInclusive;
}
public boolean contains(int pos) {
return startIndexInclusive <= pos && endIndexInclusive >= pos;
}
public int getMatchLength() {
return endIndexInclusive - startIndexInclusive + 1;
}
public boolean isForwardDirection() {
return forwardDirection;
}
@Override
public String toString() {
return searchText + "[" + fieldsToString() + "]";
return text + "[" + fieldsToString() + "]";
}
protected String fieldsToString() {
return startIndexInclusive + ", end=" + endIndexInclusive;
return "line=%s, start=%s, end=%s".formatted(lineNumber, startIndexInclusive,
endIndexInclusive);
}
}

View File

@@ -1,539 +0,0 @@
/* ###
* 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;
import java.awt.*;
import java.util.Collection;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.regex.*;
import javax.swing.JEditorPane;
import javax.swing.event.*;
import javax.swing.text.*;
import generic.theme.GColor;
import ghidra.util.Msg;
import ghidra.util.UserSearchUtils;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.*;
/**
* A class to find text matches in the given {@link TextComponent}. This class will search for all
* matches and cache the results for future requests when the user presses Next or Previous. All
* matches will be highlighted in the text component. The match containing the cursor will be a
* different highlight color than the others. When the find dialog is closed, all highlights are
* removed.
*/
public class TextComponentSearcher implements FindDialogSearcher {
private Color highlightColor = new GColor("color.bg.find.highlight");
private Color activeHighlightColor = new GColor("color.bg.find.highlight.active");
private JEditorPane editorPane;
private DocumentListener documentListener = new DocumentChangeListener();
private CaretListener caretListener = new CaretChangeListener();
private SwingUpdateManager caretUpdater = new SwingUpdateManager(() -> updateActiveHighlight());
private volatile boolean isUpdatingCaretInternally;
private SearchResults searchResults;
public TextComponentSearcher(JEditorPane editorPane) {
this.editorPane = editorPane;
if (editorPane == null) {
return; // some clients initialize without an editor pane
}
Document document = editorPane.getDocument();
document.addDocumentListener(documentListener);
editorPane.addCaretListener(caretListener);
}
public void setEditorPane(JEditorPane editorPane) {
if (this.editorPane != editorPane) {
Document document = editorPane.getDocument();
document.removeDocumentListener(documentListener);
markResultsStale();
}
this.editorPane = editorPane;
}
public JEditorPane getEditorPane() {
return editorPane;
}
@Override
public void dispose() {
caretUpdater.dispose();
if (editorPane != null) {
Document document = editorPane.getDocument();
document.removeDocumentListener(documentListener);
clearHighlights();
}
}
@Override
public void clearHighlights() {
if (searchResults != null) {
searchResults.removeHighlights();
searchResults = null;
}
}
public boolean hasSearchResults() {
return searchResults != null && !searchResults.isEmpty();
}
public boolean isStale() {
return searchResults != null && searchResults.isStale();
}
private void markResultsStale() {
if (searchResults != null) {
searchResults.setStale();
}
}
private void updateActiveHighlight() {
if (searchResults == null) {
return;
}
int pos = editorPane.getCaretPosition();
searchResults.updateActiveMatch(pos);
}
private void setCaretPositionInternally(int pos) {
isUpdatingCaretInternally = true;
try {
editorPane.setCaretPosition(pos);
}
finally {
isUpdatingCaretInternally = false;
}
}
@Override
public CursorPosition getCursorPosition() {
int pos = editorPane.getCaretPosition();
return new CursorPosition(pos);
}
@Override
public void setCursorPosition(CursorPosition position) {
int pos = position.getPosition();
editorPane.setCaretPosition(pos);
}
@Override
public CursorPosition getStart() {
return new CursorPosition(0);
}
@Override
public CursorPosition getEnd() {
int length = editorPane.getDocument().getLength();
return new CursorPosition(length - 1);
}
@Override
public void highlightSearchResults(SearchLocation location) {
if (location == null) {
clearHighlights();
return;
}
TextComponentSearchLocation textLocation = (TextComponentSearchLocation) location;
FindMatch match = textLocation.getMatch();
searchResults.setActiveMatch(match);
}
@Override
public SearchLocation search(String text, CursorPosition cursorPosition,
boolean searchForward, boolean useRegex) {
updateSearchResults(text, useRegex);
int pos = cursorPosition.getPosition();
int searchStart = getSearchStart(pos, searchForward);
FindMatch match = searchResults.getNextMatch(searchStart, searchForward);
if (match == null) {
return null;
}
return new TextComponentSearchLocation(match.getStart(), match.getEnd(), text,
searchForward, match);
}
private void updateSearchResults(String text, boolean useRegex) {
if (searchResults != null) {
if (!searchResults.isInvalid(text)) {
return; // the current results are still valid
}
searchResults.removeHighlights();
}
SearchTask searchTask = new SearchTask(text, useRegex);
TaskLauncher.launch(searchTask);
searchResults = searchTask.getSearchResults();
searchResults.applyHighlights();
}
private int getSearchStart(int startPosition, boolean isForward) {
FindMatch activeMatch = searchResults.getActiveMatch();
if (activeMatch == null) {
return startPosition;
}
int lastMatchStart = activeMatch.getStart();
if (startPosition != lastMatchStart) {
return startPosition;
}
// Always prefer the caret position, unless it aligns with the previous match. By
// moving it forward one we will continue our search, as opposed to always matching
// the same hit.
if (isForward) {
return startPosition + 1;
}
// backwards
if (startPosition == 0) {
return editorPane.getText().length();
}
return startPosition - 1;
}
//=================================================================================================
// Inner Classes
//=================================================================================================
private class SearchResults {
private TreeMap<Integer, FindMatch> matchesByPosition;
private FindMatch activeMatch;
private boolean isStale;
private String searchText;
SearchResults(String searchText, TreeMap<Integer, FindMatch> matchesByPosition) {
this.searchText = searchText;
this.matchesByPosition = matchesByPosition;
}
boolean isStale() {
return isStale;
}
void updateActiveMatch(int pos) {
if (activeMatch != null) {
activeMatch.setActive(false);
activeMatch = null;
}
if (isStale) {
// not way to easily change highlights for the caret position while we are stale,
// since the matches no longer match the document positions
return;
}
for (FindMatch match : matchesByPosition.values()) {
boolean isActive = false;
if (match.contains(pos)) {
activeMatch = match;
isActive = true;
}
match.setActive(isActive);
}
}
FindMatch getActiveMatch() {
return activeMatch;
}
FindMatch getNextMatch(int searchStart, boolean searchForward) {
Entry<Integer, FindMatch> entry;
if (searchForward) {
entry = matchesByPosition.ceilingEntry(searchStart);
}
else {
entry = matchesByPosition.floorEntry(searchStart);
}
if (entry == null) {
return null; // no more matches in the current direction
}
return entry.getValue();
}
boolean isEmpty() {
return matchesByPosition.isEmpty();
}
void setStale() {
isStale = true;
}
boolean isInvalid(String otherSearchText) {
if (isStale) {
return true;
}
return !searchText.equals(otherSearchText);
}
void setActiveMatch(FindMatch match) {
if (activeMatch != null) {
activeMatch.setActive(false);
}
activeMatch = match;
activeMatch.activate();
}
void applyHighlights() {
Collection<FindMatch> matches = matchesByPosition.values();
for (FindMatch match : matches) {
match.applyHighlight();
}
}
void removeHighlights() {
activeMatch = null;
JEditorPane editor = editorPane;
Highlighter highlighter = editor.getHighlighter();
if (highlighter != null) {
highlighter.removeAllHighlights();
}
matchesByPosition.clear();
}
}
private class TextComponentSearchLocation extends SearchLocation {
private FindMatch match;
public TextComponentSearchLocation(int start, int end,
String searchText, boolean forwardDirection, FindMatch match) {
super(start, end, searchText, forwardDirection);
this.match = match;
}
FindMatch getMatch() {
return match;
}
}
private class SearchTask extends Task {
private String searchText;
private TreeMap<Integer, FindMatch> searchHits = new TreeMap<>();
private boolean useRegex;
SearchTask(String searchText, boolean useRegex) {
super("Help Search Task", true, false, true, true);
this.searchText = searchText;
this.useRegex = useRegex;
}
@Override
public void run(TaskMonitor monitor) throws CancelledException {
String screenText;
try {
Document document = editorPane.getDocument();
screenText = document.getText(0, document.getLength());
}
catch (BadLocationException e) {
Msg.error(this, "Unable to get text for user find operation", e);
return;
}
Pattern pattern = createSearchPattern(searchText, useRegex);
Matcher matcher = pattern.matcher(screenText);
while (matcher.find()) {
monitor.checkCancelled();
int start = matcher.start();
int end = matcher.end();
FindMatch match = new FindMatch(searchText, start, end);
searchHits.put(start, match);
}
}
private Pattern createSearchPattern(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, editorPane, "Regular Expression Syntax Error",
e.getMessage());
return null;
}
}
return UserSearchUtils.createPattern(searchString, false, options);
}
SearchResults getSearchResults() {
return new SearchResults(searchText, searchHits);
}
}
private class FindMatch {
private String text;
private int start;
private int end;
private boolean isActive;
// this tag is a way to remove an installed highlight
private Object lastHighlightTag;
FindMatch(String text, int start, int end) {
this.start = start;
this.end = end;
this.text = text;
}
boolean contains(int pos) {
// exclusive of end so the cursor behind the match does is not in the highlight
return start <= pos && pos < end;
}
/** Calls setActive() and moves the caret position */
void activate() {
setActive(true);
setCaretPositionInternally(start);
scrollToVisible();
}
/**
* Makes this match active and updates the highlight color
* @param b true for active
*/
void setActive(boolean b) {
isActive = b;
applyHighlight();
}
int getStart() {
return start;
}
int getEnd() {
return end;
}
void scrollToVisible() {
try {
Rectangle startR = editorPane.modelToView2D(start).getBounds();
Rectangle endR = editorPane.modelToView2D(end).getBounds();
endR.width += 20; // a little extra space so the view is not right at the text end
Rectangle union = startR.union(endR);
editorPane.scrollRectToVisible(union);
}
catch (BadLocationException e) {
Msg.debug(this, "Exception scrolling to text", e);
}
}
@Override
public String toString() {
return "[" + start + ',' + end + "] " + text;
}
void applyHighlight() {
Highlighter highlighter = editorPane.getHighlighter();
if (highlighter == null) {
highlighter = new DefaultHighlighter();
editorPane.setHighlighter(highlighter);
}
Highlighter.HighlightPainter painter =
new DefaultHighlighter.DefaultHighlightPainter(
isActive ? activeHighlightColor : highlightColor);
try {
if (lastHighlightTag != null) {
highlighter.removeHighlight(lastHighlightTag);
}
lastHighlightTag = highlighter.addHighlight(start, end, painter);
}
catch (BadLocationException e) {
Msg.debug(this, "Exception adding highlight", e);
}
}
}
private class DocumentChangeListener implements DocumentListener {
@Override
public void insertUpdate(DocumentEvent e) {
// this allows the previous search results to stay visible until a new find is requested
markResultsStale();
}
@Override
public void removeUpdate(DocumentEvent e) {
markResultsStale();
}
@Override
public void changedUpdate(DocumentEvent e) {
// ignore attribute changes since they don't affect the text content
}
}
private class CaretChangeListener implements CaretListener {
private int lastPos = -1;
@Override
public void caretUpdate(CaretEvent e) {
int pos = e.getDot();
if (isUpdatingCaretInternally) {
lastPos = pos;
return;
}
if (pos == lastPos) {
return;
}
lastPos = pos;
caretUpdater.update();
}
}
}

View File

@@ -13,11 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking.widgets;
package docking.widgets.search;
import java.util.List;
import javax.help.UnsupportedOperationException;
import docking.widgets.CursorPosition;
import docking.widgets.FindDialog;
/**
* A simple interface for the {@link FindDialog} so that it can work for different search clients.
@@ -25,6 +24,10 @@ import javax.help.UnsupportedOperationException;
* 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.
* <p>
* The {@link FindDialog} should use a single searcher for the life of the dialog. This allows all
* search results generated by that dialog to share the same worker queue for running background
* operations related to managing search results.
*/
public interface FindDialogSearcher {
@@ -34,12 +37,6 @@ public interface FindDialogSearcher {
*/
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.
@@ -54,45 +51,28 @@ public interface FindDialogSearcher {
*/
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);
/**
* Clears any active highlights.
*/
public void clearHighlights();
/**
* 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 true if the search text is a regular expression; false if the texts is
* literal text.
* @param useRegex true if the search text is a regular expression; false if the text is literal.
* @return the search result or null if no match was found.
*/
public SearchLocation search(String text, CursorPosition cursorPosition, boolean searchForward,
public SearchResults 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.
* @param useRegex true if the search text is a regular expression; false if the text is literal.
* @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");
}
public SearchResults searchAll(String text, boolean useRegex);
/**
* Disposes this searcher. This does nothing by default.
* Disposes this searcher.
*/
public default void dispose() {
// stub
}
public void dispose();
}

View File

@@ -13,25 +13,26 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.navigation.locationreferences;
package docking.widgets.search;
import java.util.*;
import docking.widgets.SearchLocation;
import generic.json.Json;
import ghidra.util.HTMLUtilities;
/**
* A class to hold context representation for {@link LocationReference}s.
* A class to hold context representation for {@link SearchLocation}s.
*
* @see LocationReferenceContextBuilder
* @see SearchLocationContextBuilder
*/
public class LocationReferenceContext {
public class SearchLocationContext {
private static final String EMBOLDEN_START =
"<span style=\"background-color: #a3e4d7; color: black;\"><b><font size=4>";
private static final String EMBOLDEN_END = "</font></b></span>";
public static final LocationReferenceContext EMPTY_CONTEXT = new LocationReferenceContext();
public static final SearchLocationContext EMPTY_CONTEXT = new SearchLocationContext();
private final List<Part> parts;
@@ -42,8 +43,8 @@ public class LocationReferenceContext {
* @param text the text
* @return the context
*/
public static LocationReferenceContext get(String text) {
return text == null ? EMPTY_CONTEXT : new LocationReferenceContext(text);
public static SearchLocationContext get(String text) {
return text == null ? EMPTY_CONTEXT : new SearchLocationContext(text);
}
/**
@@ -51,14 +52,14 @@ public class LocationReferenceContext {
* @param context the context to verify is not null
* @return the given context or the {@link #EMPTY_CONTEXT} if the given context is null
*/
public static LocationReferenceContext get(LocationReferenceContext context) {
public static SearchLocationContext get(SearchLocationContext context) {
return context == null ? EMPTY_CONTEXT : context;
}
/**
* Creates an empty context object
*/
private LocationReferenceContext() {
private SearchLocationContext() {
this.parts = List.of(new BasicPart(""));
}
@@ -66,7 +67,7 @@ public class LocationReferenceContext {
* Creates a context with the raw and decorated context being the same.
* @param context the context; cannot be null
*/
private LocationReferenceContext(String context) {
private SearchLocationContext(String context) {
Objects.requireNonNull(context);
this.parts = List.of(new BasicPart(context));
}
@@ -74,9 +75,9 @@ public class LocationReferenceContext {
/**
* Constructor used to create this context by providing the given text parts
* @param parts the parts
* @see LocationReferenceContextBuilder
* @see SearchLocationContextBuilder
*/
LocationReferenceContext(List<Part> parts) {
SearchLocationContext(List<Part> parts) {
this.parts = parts;
}
@@ -120,7 +121,7 @@ public class LocationReferenceContext {
/**
* Returns any sub-strings of this context's overall text that match client-defined input
*
* See the {@link LocationReferenceContextBuilder} for how to define matching text pieces
* See the {@link SearchLocationContextBuilder} for how to define matching text pieces
* @return the matching strings
*/
public List<String> getMatches() {

View File

@@ -13,20 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.navigation.locationreferences;
package docking.widgets.search;
import java.util.ArrayList;
import java.util.List;
import docking.widgets.search.SearchLocationContext.*;
import generic.json.Json;
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext.*;
/**
* A builder for {@link LocationReferenceContext} objects. Use {@link #append(String)} for normal
* A builder for {@link SearchLocationContext} objects. Use {@link #append(String)} for normal
* text pieces. Use {@link #appendMatch(String)} for text that is meant to be rendered specially
* by the context class.
*/
public class LocationReferenceContextBuilder {
public class SearchLocationContextBuilder {
private List<Part> parts = new ArrayList<>();
@@ -35,7 +35,7 @@ public class LocationReferenceContextBuilder {
* @param text the text
* @return this builder
*/
public LocationReferenceContextBuilder append(String text) {
public SearchLocationContextBuilder append(String text) {
if (text == null) {
text = "";
}
@@ -45,12 +45,12 @@ public class LocationReferenceContextBuilder {
/**
* Appends the given text to this builder. This text represents a client-defined 'match' that
* will be rendered with markup when {@link LocationReferenceContext#getBoldMatchingText()} is
* will be rendered with markup when {@link SearchLocationContext#getBoldMatchingText()} is
* called.
* @param text the text
* @return this builder
*/
public LocationReferenceContextBuilder appendMatch(String text) {
public SearchLocationContextBuilder appendMatch(String text) {
if (text == null) {
throw new NullPointerException("Match text cannot be null");
}
@@ -62,7 +62,7 @@ public class LocationReferenceContextBuilder {
* Adds a newline character to the previously added text.
* @return this builder
*/
public LocationReferenceContextBuilder newline() {
public SearchLocationContextBuilder newline() {
if (parts.isEmpty()) {
throw new IllegalStateException("Cannot add a newline without first appending text");
}
@@ -72,12 +72,12 @@ public class LocationReferenceContextBuilder {
}
/**
* Builds a {@link LocationReferenceContext} using the text supplied via the {@code append}
* Builds a {@link SearchLocationContext} using the text supplied via the {@code append}
* methods.
* @return the context
*/
public LocationReferenceContext build() {
return new LocationReferenceContext(parts);
public SearchLocationContext build() {
return new SearchLocationContext(parts);
}
/**

View File

@@ -0,0 +1,302 @@
/* ###
* 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.net.URL;
import java.time.Duration;
import java.util.List;
import java.util.function.BooleanSupplier;
import docking.widgets.FindDialog;
import docking.widgets.SearchLocation;
import ghidra.util.Msg;
import ghidra.util.Swing;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.MonitoredRunnable;
import ghidra.util.task.TaskMonitor;
import ghidra.util.worker.Job;
import ghidra.util.worker.Worker;
/**
* A collection of {@link SearchLocation}s created when the user has performed a find operation on
* the {@link FindDialog}. The dialog will find all results and then use the results to move to the
* next and previous locations as requested. The user may also choose to show all results in a
* table.
* <p>
* The searcher uses a worker queue to manage activating and deactivating highlights, which may
* require reload operations on the originally searched text.
*/
public abstract class SearchResults {
private Worker worker;
protected SearchResults(Worker worker) {
this.worker = worker;
}
Worker getWorker() {
return worker;
}
//=================================================================================================
// Abstract Methods
//=================================================================================================
/**
* Returns the name of this set of search results. This is a short description, such as a
* filename or function name. This should be null for text components that do not change
* contents based on some external source of data, such as a file.
* @return the name or null
*/
public abstract String getName();
/**
* Activates this set of search results. This will restore highlights to the source of the
* search.
*/
public abstract void activate();
/**
* Deactivates this set of search results. This will clear this results' highlights from the
* source of the search.
*/
public abstract void deactivate();
/**
* Sets the active location, which will be highlighted differently than the other search
* matches. This method will ensure that this search results object is active (see
* {@link #activate()}. This method will also move the cursor to the given location.
*
* @param location the location
*/
public abstract void setActiveLocation(SearchLocation location);
/**
* {@return the active search location or null. The active location is typically the search
* location that contains the user's cursor.}
*/
public abstract SearchLocation getActiveLocation();
/**
* Returns all search locations in this set of search results
* @return the location
*/
public abstract List<SearchLocation> getLocations();
public abstract boolean isEmpty();
public abstract void dispose();
//=================================================================================================
// End Abstract Methods
//=================================================================================================
protected String getFilename(URL url) {
if (url == null) {
return null;
}
String path = url.getPath();
int index = path.lastIndexOf('/');
if (index < 0) {
return null;
}
return path.substring(index + 1); // +1 to not get the slash
}
/**
* Clears all jobs that have the same class as the given job. Clients can call this method
* before submitting the given job to clear any other instances of that job type before running.
* @param job the job
*/
protected void cancelAllJobsOfType(FindJob job) {
Class<? extends FindJob> clazz = job.getClass();
worker.clearAllJobs(j -> j.getClass() == clazz);
}
/**
* Runs the given activation job. This class will cancel any existing activation jobs with the
* assumption that only one activation should be taking place at any given time. This is useful
* since activations may be slow.
* @param job the job
*/
protected void runActivationJob(ActivationJob job) {
cancelAllJobsOfType(job);
runJob(job);
}
/**
* Runs the given job, cancelling any currently running jobs. We assume that only one job
* should be run at a time for the given worker for all search results sharing that worker,
* not just a single search results. This keeps multiple search results from interfering with
* each other as the user interacts with the results.
* @param job the job
*/
protected void runJob(FindJob job) {
worker.schedule(job);
}
/**
* A worker {@link Job} that allows subclasses to add follow-on jobs to be performed as long
* as the work is not cancelled.
*/
protected class FindJob extends Job {
// The parent job in a chain of jobs. Useful for debugging.
protected FindJob parent;
// optional follow-on job
protected FindJob nextJob;
// optional runnable to be called instead of doRun()
protected MonitoredRunnable runnable;
public FindJob() {
// no runnable; use doRun()
}
public FindJob(FindJob parent) {
this.parent = parent;
}
public FindJob(FindJob parent, MonitoredRunnable r) {
this.parent = parent;
this.runnable = r;
}
@Override
public final void run(TaskMonitor monitor) throws CancelledException {
monitor.checkCancelled();
if (runnable != null) {
runnable.monitoredRun(monitor);
}
else {
doRun(monitor);
}
monitor.checkCancelled();
if (nextJob != null) {
nextJob.run(monitor);
}
}
@SuppressWarnings("unused") // we don't use the cancel, but subclasses may
protected void doRun(TaskMonitor monitor) throws CancelledException {
// clients override to do background work
}
public FindJob thenRun(MonitoredRunnable r) {
FindJob job = new FindJob(this, r);
setNextJob(job);
return this;
}
public FindJob thenWait(BooleanSupplier waitFor, Duration maxWaitTime) {
FindJob job = new WaitJob(this, waitFor, maxWaitTime);
setNextJob(job);
return this;
}
public FindJob thenRunSwing(Runnable r) {
MonitoredRunnable swingRunnable = m -> {
Swing.runNow(() -> {
if (m.isCancelled()) {
return;
}
r.run();
});
};
FindJob job = new FindJob(this, swingRunnable);
setNextJob(job);
return this;
}
private void setNextJob(FindJob job) {
if (nextJob == null) {
nextJob = job;
}
else {
nextJob.setNextJob(job);
}
}
@Override
public String toString() {
String base = getClass().getSimpleName() + ' ' + SearchResults.this;
if (runnable != null) {
return "runnable-only " + base;
}
return base;
}
}
public class ActivationJob extends FindJob {
// nothing special to do here; just a marker class
}
public class SwingJob extends FindJob {
public SwingJob(Runnable r) {
this.runnable = m -> r.run();
}
}
private class WaitJob extends FindJob {
private BooleanSupplier waitFor;
private Duration maxWaitTime;
protected WaitJob(FindJob parent, BooleanSupplier waitFor, Duration maxWaitTime) {
super(parent);
this.waitFor = waitFor;
this.maxWaitTime = maxWaitTime;
}
@Override
protected void doRun(TaskMonitor monitor) throws CancelledException {
int sleepyTime = 250;
int totalMs = 0;
while (totalMs < maxWaitTime.toMillis()) {
monitor.checkCancelled();
if (waitFor.getAsBoolean()) {
return;
}
totalMs += sleepyTime;
sleep(sleepyTime);
}
monitor.cancel();
throw new CancelledException();
}
private void sleep(int sleepyTime) throws CancelledException {
try {
Thread.sleep(sleepyTime);
}
catch (InterruptedException e) {
Msg.debug(this, "Find job interrupted while waiting");
throw new CancelledException();
}
}
}
}

View File

@@ -0,0 +1,47 @@
/* ###
* 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 docking.widgets.SearchLocation;
public class TextComponentSearchLocation extends SearchLocation {
private boolean isActive;
private Object lastHighlightTag;
TextComponentSearchLocation(String searchText, int startInclusive, int endInclusive,
int lineNumber, SearchLocationContext context) {
super(startInclusive, endInclusive, searchText, lineNumber, context);
}
void setActive(boolean b) {
isActive = b;
}
boolean isActive() {
return isActive;
}
// sets the Highlighter created that allows for removal of the highlight for this match
void setHighlightTag(Object tag) {
this.lastHighlightTag = tag;
}
Object getHighlightTag() {
return lastHighlightTag;
}
}

View File

@@ -0,0 +1,616 @@
/* ###
* 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 java.net.URL;
import java.util.*;
import java.util.List;
import java.util.Map.Entry;
import javax.swing.JEditorPane;
import javax.swing.event.*;
import javax.swing.text.*;
import javax.swing.text.DefaultHighlighter.DefaultHighlightPainter;
import javax.swing.text.Highlighter.Highlight;
import javax.swing.text.Highlighter.HighlightPainter;
import docking.widgets.SearchLocation;
import generic.theme.GColor;
import ghidra.util.Msg;
import ghidra.util.Swing;
import ghidra.util.task.SwingUpdateManager;
import ghidra.util.worker.Worker;
import util.CollectionUtils;
public class TextComponentSearchResults extends SearchResults {
private Color highlightColor = new GColor("color.bg.find.highlight");
private Color activeHighlightColor = new GColor("color.bg.find.highlight.active");
protected JEditorPane editorPane;
private SearchResultsHighlighterWrapper highlighter;
private DocumentListener documentListener = new DocumentChangeListener();
private CaretListener caretListener = new CaretChangeListener();
private SwingUpdateManager caretUpdater =
new SwingUpdateManager(() -> setActiveHighlightBasedOnCaret());
private boolean isUpdatingCaretInternally;
private String name;
private List<TextComponentSearchLocation> searchLocations;
private TreeMap<Integer, TextComponentSearchLocation> matchesByPosition;
private String searchText;
private TextComponentSearchLocation activeLocation;
/**
* Stale means the document has changed and our location offsets may no longer match. Once
* stale, always stale.
*/
private boolean isStale;
protected TextComponentSearchResults(Worker worker, JEditorPane editorPane, String searchText,
TreeMap<Integer, TextComponentSearchLocation> matchesByPosition) {
super(worker);
this.editorPane = editorPane;
this.searchText = searchText;
this.matchesByPosition = matchesByPosition;
URL url = editorPane.getPage();
this.name = getFilename(url);
Collection<TextComponentSearchLocation> matches = matchesByPosition.values();
this.searchLocations = new ArrayList<>(matches);
Document document = editorPane.getDocument();
document.addDocumentListener(documentListener);
editorPane.addCaretListener(caretListener);
// All results will be highlighted. Since we don't move the caret to a specific match, make
// sure that the highlight color gets updated based on the current caret position.
caretUpdater.updateLater();
highlighter = createHighlighter();
}
@Override
public String getName() {
return name;
}
@Override
public void deactivate() {
if (isActive()) {
FindJob job = new SwingJob(this::unapplyHighlights);
runJob(job);
}
}
/**
* Triggers the potentially asynchronous activation of this set of search results. When that is
* finished, we then restore our highlights. This is needed in the case that the implementor
* is using a document that does not match our search results. Some subclasses use
* asynchronous loading of their document.
*/
@Override
public void activate() {
FindJob job = startActivation();
runJob(job);
}
@Override
public void setActiveLocation(SearchLocation location) {
if (isStale) {
return;
}
if (activeLocation == location) {
return;
}
if (location == null) {
// no need to activate these results when clearing the active location
Swing.runNow(() -> doSetActiveLocation(null));
return;
}
FindJob job = startActivation().thenRunSwing(() -> doSetActiveLocation(location));
runActivationJob((ActivationJob) job);
}
/**
* Create a job to perform activation for this class. The activation job may be a 'done' job
* if not activation is required.
* @return the job
*/
protected ActivationJob startActivation() {
if (isActive()) {
return createFinishedActivationJob();
}
if (isStale) {
unapplyHighlights();
return createFinishedActivationJob();
}
return (ActivationJob) createActivationJob().thenRunSwing(() -> applyHighlights());
}
/**
* Starts the job that will activate this class. Subclasses can override this method to change
* the job that gets run.
* @return the job
*/
protected ActivationJob createActivationJob() {
return new ActivationJob();
}
protected ActivationJob createFinishedActivationJob() {
return new ActivationJob();
}
private void doSetActiveLocation(SearchLocation newLocation) {
TextComponentSearchLocation oldLocation = activeLocation;
activeLocation = (TextComponentSearchLocation) newLocation;
if (oldLocation == newLocation) {
scrollToLocation(activeLocation); // this handles null
return;
}
changeActiveLocation(oldLocation, activeLocation);
}
TextComponentSearchLocation getNextLocation(int searchStart, boolean searchForward) {
Entry<Integer, TextComponentSearchLocation> entry;
if (searchForward) {
entry = matchesByPosition.ceilingEntry(searchStart);
}
else {
entry = matchesByPosition.floorEntry(searchStart);
}
if (entry == null) {
return null; // no more matches in the current direction
}
return entry.getValue();
}
@Override
public boolean isEmpty() {
return searchLocations.isEmpty();
}
@Override
public List<SearchLocation> getLocations() {
return CollectionUtils.asList(searchLocations, SearchLocation.class);
}
boolean isStale() {
return isStale;
}
void setStale() {
isStale = true;
unapplyHighlights();
}
protected boolean isInvalid(String otherSearchText) {
if (isStale) {
return true;
}
return !searchText.equals(otherSearchText);
}
@Override
public SearchLocation getActiveLocation() {
return activeLocation;
}
private void updateActiveLocationForCaretChange(int caret) {
TextComponentSearchLocation location = getLocation(caret);
setActiveLocation(location);
}
private TextComponentSearchLocation getLocation(int caret) {
Optional<TextComponentSearchLocation> optional =
searchLocations.stream().filter(l -> l.contains(caret)).findFirst();
return optional.orElseGet(() -> null);
}
private void changeActiveLocation(TextComponentSearchLocation oldLocation,
TextComponentSearchLocation newLocation) {
clearActiveHighlight(oldLocation);
if (isStale) {
// no way to easily change highlights for the caret position while we are stale,
// since the locations no longer match the document positions
return;
}
if (newLocation == null) {
return;
}
newLocation.setActive(true);
doHighlightLocation(newLocation);
scrollToLocation(newLocation);
}
private void clearActiveHighlight(TextComponentSearchLocation location) {
if (location == null) {
return;
}
location.setActive(false);
doHighlightLocation(location); // turn off the active highlight
}
private void scrollToLocation(TextComponentSearchLocation location) {
if (location == null) {
return;
}
int caret = editorPane.getCaretPosition();
if (!location.contains(caret)) {
setCaretPositionInternally(location);
}
scrollToVisible(location);
}
private void setCaretPositionInternally(TextComponentSearchLocation location) {
if (isStale) {
// once the document contents have changed, we have know way of knowing if the matches
// are still valid
return;
}
Document doc = editorPane.getDocument();
int len = doc.getLength();
if (len == 0) {
// This can happen if the document is getting loaded asynchronously. We work around
// this elsewhere, making this a very low occurrence event. If it happens, just
// ignore the caret update.
return;
}
isUpdatingCaretInternally = true;
try {
int pos = location.getStartIndexInclusive();
editorPane.setCaretPosition(pos);
}
finally {
isUpdatingCaretInternally = false;
}
}
private void scrollToVisible(TextComponentSearchLocation location) {
try {
int start = location.getStartIndexInclusive();
int end = location.getEndIndexInclusive();
Rectangle startR = editorPane.modelToView2D(start).getBounds();
Rectangle endR = editorPane.modelToView2D(end).getBounds();
endR.width += 20; // a little extra space so the view is not right at the text end
Rectangle union = startR.union(endR);
editorPane.scrollRectToVisible(union);
}
catch (BadLocationException e) {
Msg.debug(this, "Exception scrolling to text", e);
}
}
private void setActiveHighlightBasedOnCaret() {
if (!isActive()) {
return;
}
int pos = editorPane.getCaretPosition();
updateActiveLocationForCaretChange(pos);
}
/**
* Creates our search highlighter, wrapping any existing highlighter in order to not lose
* client highlights.
* @return the new highlighter
*/
private SearchResultsHighlighterWrapper createHighlighter() {
Highlighter activeHighlighter = editorPane.getHighlighter();
if (activeHighlighter == null) {
return new SearchResultsHighlighterWrapper(null);
}
if (activeHighlighter instanceof SearchResultsHighlighterWrapper wrapper) {
// don't wrap another search highlighter, as we will check for them later
return new SearchResultsHighlighterWrapper(wrapper.delegate);
}
// some other client non-search highlighter
return new SearchResultsHighlighterWrapper(activeHighlighter);
}
private SearchResultsHighlighterWrapper getInstalledSearchResultsHighlighter() {
Highlighter activeHighlighter = editorPane.getHighlighter();
if (activeHighlighter == null) {
return null;
}
if (activeHighlighter instanceof SearchResultsHighlighterWrapper wrapper) {
return wrapper;
}
// some other client non-search highlighter
return null;
}
/*
We used to use a boolean to track the active state. However, due to how clients activate
and deactivate, the boolean could get out-of-sync with the highlighter. Thus, use the
active highlighter as the method for checking if we are active.
*/
boolean isActive() {
SearchResultsHighlighterWrapper activeSearchHighlighter =
getInstalledSearchResultsHighlighter();
return activeSearchHighlighter == highlighter;
}
Highlight[] getHighlights() {
return highlighter.getHighlights();
}
private void maybeInstallHighlighter() {
SearchResultsHighlighterWrapper activeHighlighter =
getInstalledSearchResultsHighlighter();
if (activeHighlighter == highlighter) {
// we are already installed
return;
}
if (activeHighlighter != null) {
// another search highlighter is installed
activeHighlighter.removeAllHighlights();
}
editorPane.setHighlighter(highlighter);
}
private void applyHighlights() {
unapplyHighlights();
// Any other search highlights will be cleared when we install our highlighter
maybeInstallHighlighter();
Collection<TextComponentSearchLocation> locations = matchesByPosition.values();
for (TextComponentSearchLocation location : locations) {
doHighlightLocation(location);
}
setActiveHighlightBasedOnCaret();
}
/**
* Clears highlights, but does not remove known matches. This allows highlights to later be
* restored.
*/
private void unapplyHighlights() {
// reset and repaint the active highlight
setActiveLocation(null);
Highlighter activeHighlighter = editorPane.getHighlighter();
if (activeHighlighter == highlighter) {
// only remove our highlights
highlighter.removeAllHighlights();
highlighter.uninstall();
}
}
private void doHighlightLocation(TextComponentSearchLocation location) {
Highlighter activeHighlighter = editorPane.getHighlighter();
if (activeHighlighter != highlighter) {
// Not our highlighter; don't change highlights. Shouldn't happen.
return;
}
Object tag = location.getHighlightTag();
if (tag != null) {
// always remove any previous highlight before adding a new one
highlighter.removeHighlight(tag);
}
if (isStale) {
return; // do not highlight when stale
}
Color c = location.isActive() ? activeHighlightColor : highlightColor;
HighlightPainter painter = new DefaultHighlightPainter(c);
int start = location.getStartIndexInclusive();
int end = location.getEndIndexInclusive() + 1; // +1 to make inclusive be exclusive
try {
tag = highlighter.addHighlight(start, end, painter);
location.setHighlightTag(tag);
}
catch (BadLocationException e) {
Msg.debug(this, "Exception adding highlight", e);
}
}
@Override
public void dispose() {
deactivate();
caretUpdater.dispose();
if (editorPane != null) {
Document document = editorPane.getDocument();
document.removeDocumentListener(documentListener);
}
matchesByPosition.clear();
searchLocations.clear();
highlighter.uninstall();
isStale = true;
}
@Override
public String toString() {
return "%s: %s (%s)".formatted(getClass().getSimpleName(), searchText,
System.identityHashCode(this));
}
//=================================================================================================
// Inner Classes
//=================================================================================================
private class DocumentChangeListener implements DocumentListener {
@Override
public void insertUpdate(DocumentEvent e) {
// this allows the previous search results to stay visible until a new find is requested
setStale();
}
@Override
public void removeUpdate(DocumentEvent e) {
setStale();
}
@Override
public void changedUpdate(DocumentEvent e) {
// ignore attribute changes since they don't affect the text content
}
}
private class CaretChangeListener implements CaretListener {
private int lastPos = -1;
@Override
public void caretUpdate(CaretEvent e) {
int pos = e.getDot();
if (isUpdatingCaretInternally) {
lastPos = pos;
return;
}
if (pos == lastPos) {
return;
}
lastPos = pos;
caretUpdater.update();
}
}
/**
* A class that allows us to replace any already installed highlighter. This also allows us to
* add and remove highlighters, depending upon the active search.
* <p>
* Note: any non-search highlighters installed after this wrapper is created may be overwritten
* as the usr interacts with the search.
*/
private class SearchResultsHighlighterWrapper extends DefaultHighlighter {
private Highlighter delegate;
private boolean nonSearchDelegate;
SearchResultsHighlighterWrapper(Highlighter delegate) {
if (delegate == null) {
delegate = new DefaultHighlighter();
nonSearchDelegate = false;
}
else {
nonSearchDelegate = true;
}
this.delegate = delegate;
}
void uninstall() {
Highlighter activeHighlighter = editorPane.getHighlighter();
if (activeHighlighter != this) {
return;
}
if (nonSearchDelegate) {
editorPane.setHighlighter(delegate);
}
else {
editorPane.setHighlighter(null);
}
}
@Override
public void install(JTextComponent c) {
delegate.install(c);
}
@Override
public void deinstall(JTextComponent c) {
delegate.deinstall(c);
}
@Override
public void paint(Graphics g) {
delegate.paint(g);
}
@Override
public void paintLayeredHighlights(Graphics g, int p0, int p1, Shape viewBounds,
JTextComponent editor, View view) {
if (delegate instanceof LayeredHighlighter lh) {
lh.paintLayeredHighlights(g, p0, p1, viewBounds, editor, view);
}
}
@Override
public Object addHighlight(int p0, int p1, HighlightPainter p) throws BadLocationException {
return delegate.addHighlight(p0, p1, p);
}
@Override
public void removeHighlight(Object tag) {
delegate.removeHighlight(tag);
}
@Override
public void removeAllHighlights() {
delegate.removeAllHighlights();
}
@Override
public void changeHighlight(Object tag, int p0, int p1) throws BadLocationException {
delegate.changeHighlight(tag, p0, p1);
}
@Override
public Highlight[] getHighlights() {
return delegate.getHighlights();
}
}
}

View File

@@ -0,0 +1,335 @@
/* ###
* 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.TextComponent;
import java.util.TreeMap;
import java.util.regex.*;
import javax.swing.JEditorPane;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import docking.widgets.CursorPosition;
import docking.widgets.SearchLocation;
import ghidra.util.Msg;
import ghidra.util.UserSearchUtils;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.*;
import ghidra.util.worker.Worker;
/**
* A class to find text matches in the given {@link TextComponent}. This class will search for all
* matches and cache the results for future requests when the user presses Next or Previous. All
* matches will be highlighted in the text component. The match containing the cursor will be a
* different highlight color than the others. When the find dialog is closed, all highlights are
* removed.
* <p>
* If {@link #searchAll(String, boolean)} is called, then the search results will not be cached, as
* they are when {@link #search(String, CursorPosition, boolean, boolean)} is used. The expectation
* is that clients will cache the search results themselves.
*/
public class TextComponentSearcher implements FindDialogSearcher {
static final int MAX_CONTEXT_CHARS = 100;
private int maxContextChars = MAX_CONTEXT_CHARS;
protected JEditorPane editorPane;
private Worker worker = Worker.createGuiWorker();
private TextComponentSearchResults searchResults;
public TextComponentSearcher(JEditorPane editorPane) {
this.editorPane = editorPane;
}
public void setEditorPane(JEditorPane editorPane) {
if (this.editorPane != editorPane) {
if (searchResults != null) {
searchResults.dispose();
searchResults = null;
}
}
this.editorPane = editorPane;
}
public JEditorPane getEditorPane() {
return editorPane;
}
void setMaxContextChars(int max) {
this.maxContextChars = max;
}
@Override
public void dispose() {
if (searchResults != null) {
searchResults.dispose();
searchResults = null;
}
}
public boolean hasSearchResults() {
return searchResults != null && !searchResults.isEmpty();
}
public boolean isStale() {
return searchResults != null && searchResults.isStale();
}
@Override
public CursorPosition getCursorPosition() {
int pos = editorPane.getCaretPosition();
return new CursorPosition(pos);
}
@Override
public CursorPosition getStart() {
return new CursorPosition(0);
}
@Override
public CursorPosition getEnd() {
int length = editorPane.getDocument().getLength();
return new CursorPosition(length - 1);
}
@Override
public TextComponentSearchResults searchAll(String text, boolean useRegex) {
return doSearch(text, useRegex);
}
@Override
public SearchResults search(String text, CursorPosition cursorPosition,
boolean searchForward, boolean useRegex) {
updateSearchResults(text, useRegex);
int pos = cursorPosition.getPosition();
int searchStart = getSearchStart(pos, searchForward);
if (searchStart == -1) {
return null; // signal no more matches in the current direction
}
TextComponentSearchLocation location =
searchResults.getNextLocation(searchStart, searchForward);
if (location == null) {
return null;
}
searchResults.setActiveLocation(location);
return searchResults;
}
private void updateSearchResults(String text, boolean useRegex) {
if (searchResults != null) {
if (!searchResults.isInvalid(text)) {
// the current results are still valid; ensure the highlights are still active
searchResults.activate();
return;
}
searchResults.dispose();
searchResults = null;
}
searchResults = doSearch(text, useRegex);
}
private TextComponentSearchResults doSearch(String text, boolean useRegex) {
SearchTask searchTask = new SearchTask(text, useRegex);
TaskLauncher.launch(searchTask);
TextComponentSearchResults newSearchResults = searchTask.doCreateSearchResults();
newSearchResults.activate();
return newSearchResults;
}
private int getSearchStart(int startPosition, boolean isForward) {
SearchLocation location = searchResults.getActiveLocation();
if (location == null) {
return startPosition;
}
int lastMatchStart = location.getStartIndexInclusive();
if (startPosition != lastMatchStart) {
return startPosition;
}
// Always prefer the caret position, unless it aligns with the previous match. By
// moving it forward one we will continue our search, as opposed to always matching
// the same hit.
if (isForward) {
int next = startPosition + 1;
int end = editorPane.getText().length();
if (next == end) {
return -1; // signal no more hits in this direction
}
return next;
}
// backwards
if (startPosition == 0) {
return -1; // signal no more hits in this direction
}
return startPosition - 1;
}
protected TextComponentSearchResults createSearchResults(
Worker theWorker, JEditorPane editor, String searchText,
TreeMap<Integer, TextComponentSearchLocation> matchesByPosition) {
return new TextComponentSearchResults(theWorker, editor, searchText, matchesByPosition);
}
//=================================================================================================
// Inner Classes
//=================================================================================================
private class SearchTask extends Task {
private String searchText;
private TreeMap<Integer, TextComponentSearchLocation> matchesByPosition = new TreeMap<>();
private boolean useRegex;
SearchTask(String searchText, boolean useRegex) {
super("Help Search Task", true, false, true, true);
this.searchText = searchText;
this.useRegex = useRegex;
}
TextComponentSearchResults doCreateSearchResults() {
return createSearchResults(worker, editorPane, searchText, matchesByPosition);
}
@Override
public void run(TaskMonitor monitor) throws CancelledException {
Document document;
String fullText;
try {
document = editorPane.getDocument();
fullText = document.getText(0, document.getLength());
}
catch (BadLocationException e) {
Msg.error(this, "Unable to get text for user find operation", e);
return;
}
TreeMap<Integer, Line> lineRangeMap = mapLines(fullText);
Pattern pattern = createSearchPattern(searchText, useRegex);
Matcher matcher = pattern.matcher(fullText);
while (matcher.find()) {
monitor.checkCancelled();
int start = matcher.start();
int end = matcher.end();
Line line = lineRangeMap.floorEntry(start).getValue();
String matchText = fullText.substring(start, end);
SearchLocationContext context = createContext(line, start, end);
TextComponentSearchLocation location =
new TextComponentSearchLocation(matchText, start, end - 1, line.lineNumber(),
context);
matchesByPosition.put(start, location);
}
}
private TreeMap<Integer, Line> mapLines(String fullText) {
TreeMap<Integer, Line> linesRangeMap = new TreeMap<>();
int lineNumber = 0;
int pos = 0;
String[] lines = fullText.split("\\n");
for (String line : lines) {
lineNumber++;
linesRangeMap.put(pos, new Line(line, lineNumber, pos));
pos += line.length() + 1; // +1 for newline
}
return linesRangeMap;
}
private SearchLocationContext createContext(Line line, int start, int end) {
SearchLocationContextBuilder builder = new SearchLocationContextBuilder();
String text = line.text();
int offset = line.offset(); // document offset
int rstart = start - offset; // line-relative start
int rend = end - offset; // line-relative end
int lineStart = 0;
int lineEnd = text.length();
int length = text.length();
int max = maxContextChars;
if (length > max) {
// HTML content can have very long lines, since it doesn't use newline characters to
// break text. We just want to show some context, so we don't need all characters.
// When the text is too long, just grab some surrounding text for the context.
int matchLength = end - start;
int remaining = max - matchLength;
int half = remaining / 2;
int firstHalf = rstart; // from 0 to match start
int available = Math.min(half, firstHalf);
int newStart = rstart - available;
available = max - (available + matchLength);
int newEnd = Math.min(length, rend + available);
lineStart = newStart;
lineEnd = newEnd;
}
if (lineStart != 0) {
builder.append("...");
}
builder.append(text.substring(lineStart, rstart));
builder.appendMatch(text.substring(rstart, rend));
if (rend < text.length()) {
builder.append(text.substring(rend, lineEnd));
}
if (lineEnd < text.length()) {
builder.append("...");
}
return builder.build();
}
private Pattern createSearchPattern(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, editorPane, "Regular Expression Syntax Error",
e.getMessage());
return null;
}
}
return UserSearchUtils.createPattern(searchString, false, options);
}
record Line(String text, int lineNumber, int offset) {
}
}
}

View File

@@ -41,7 +41,7 @@ public class DockingTabRenderer extends JPanel {
private TabContainerForwardingMouseListener forwardingListener;
private MouseListener renameListener;
public DockingTabRenderer(final JTabbedPane tabbedPane, String fullTitle, String tabTitle,
public DockingTabRenderer(final JTabbedPane tabbedPane, String fullTitle, String tabText,
ActionListener closeListener) {
final ForwardingMouseListener eventForwardingListener =
@@ -51,8 +51,8 @@ public class DockingTabRenderer extends JPanel {
iconLabel = new GDLabel();
closeButton = new EmptyBorderButton();
setTitle(tabTitle, fullTitle);
closeButton.setToolTipText("Close " + tabTitle);
setTitle(tabText, fullTitle);
closeButton.setToolTipText("Close " + tabText);
closeButton.setFocusable(false);
closeButton.addActionListener(closeListener);
closeButton.setIcon(CLOSE_ICON);
@@ -134,13 +134,13 @@ public class DockingTabRenderer extends JPanel {
iconLabel.setIcon(icon);
}
public void setTitle(String tabTitle, String fullTitle) {
titleLabel.setText(getShortenedTitle(tabTitle));
String trimmedTabText = tabTitle.trim();
public void setTitle(String tabText, String fullTitle) {
titleLabel.setText(getShortenedTitle(tabText));
String trimmedTabText = tabText.trim();
String trimmedTitleText = fullTitle.trim();
if (trimmedTabText.equals(trimmedTitleText)) {
// don't include the same text on twice
titleLabel.setToolTipText(tabTitle);
titleLabel.setToolTipText(tabText);
}
else if (trimmedTitleText.contains(trimmedTabText)) {
// don't include both when the tab text is a subset of the title
@@ -148,7 +148,7 @@ public class DockingTabRenderer extends JPanel {
}
else {
// both are different, include both
titleLabel.setToolTipText("<html><b>" + tabTitle + "</b> - [" + fullTitle + "]");
titleLabel.setToolTipText("<html><b>" + tabText + "</b> - [" + fullTitle + "]");
}
}

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.util.table.actions;
package docking.widgets.table.actions;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
@@ -24,14 +24,13 @@ import javax.swing.KeyStroke;
import javax.swing.table.TableModel;
import docking.ActionContext;
import docking.Tool;
import docking.action.*;
import docking.actions.SharedDockingActionPlaceholder;
import docking.widgets.table.GTable;
import docking.widgets.table.RowObjectTableModel;
import docking.widgets.table.threaded.ThreadedTableModel;
import generic.theme.GIcon;
import ghidra.app.util.HelpTopics;
import ghidra.framework.plugintool.PluginTool;
import ghidra.util.*;
import ghidra.util.exception.AssertException;
import ghidra.util.timer.GTimer;
@@ -46,7 +45,7 @@ import ghidra.util.timer.GTimer;
* not altering the database.
* <p>
* Tip: if you are a plugin that uses transient providers, then use
* {@link #registerDummy(PluginTool, String)} at creation time to install a dummy representative of
* {@link #registerDummy(Tool, String)} at creation time to install a dummy representative of
* this action in the Tool's options so that user's can update keybindings, regardless of whether
* they have ever shown one of your transient providers.
*/
@@ -54,7 +53,7 @@ public class DeleteTableRowAction extends DockingAction {
private static final KeyStroke DEFAULT_KEYSTROKE =
KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0);
private static final Icon ICON = new GIcon("icon.plugin.table.delete.row");
private static final Icon ICON = new GIcon("icon.widget.table.delete.row");
private static final String NAME = "Remove Items";
private GTable table;
@@ -67,7 +66,7 @@ public class DeleteTableRowAction extends DockingAction {
* @param tool the tool whose options will updated with a dummy keybinding
* @param owner the owner of the action that may be installed
*/
public static void registerDummy(PluginTool tool, String owner) {
public static void registerDummy(Tool tool, String owner) {
tool.getToolActions().registerSharedActionPlaceholder(new DeleteActionPlaceholder(owner));
}
@@ -85,7 +84,7 @@ public class DeleteTableRowAction extends DockingAction {
super(name, owner, KeyBindingType.SHARED);
setDescription("Remove the selected rows from the table");
setHelpLocation(new HelpLocation(HelpTopics.SEARCH, "Remove_Items"));
setHelpLocation(new HelpLocation("Search", "Remove_Items"));
setToolBarData(new ToolBarData(ICON, null));
setPopupMenuData(new MenuData(new String[] { "Remove Items" }, ICON, menuGroup));
@@ -166,7 +165,7 @@ public class DeleteTableRowAction extends DockingAction {
return false;
}
private void selectRow(TableModel model, final int row) {
protected void selectRow(TableModel model, final int row) {
Swing.runLater(() -> {
if (checkForBusy(model)) {

View File

Before

Width:  |  Height:  |  Size: 660 B

After

Width:  |  Height:  |  Size: 660 B

View File

@@ -21,6 +21,8 @@ import java.util.List;
import org.junit.Test;
import docking.widgets.search.FindDialogSearcher;
import docking.widgets.search.SearchResults;
import ghidra.util.Swing;
public class FindDialogTest {
@@ -43,11 +45,6 @@ public class FindDialogTest {
return new CursorPosition(0);
}
@Override
public void setCursorPosition(CursorPosition position) {
// stub
}
@Override
public CursorPosition getStart() {
return new CursorPosition(0);
@@ -59,20 +56,19 @@ public class FindDialogTest {
}
@Override
public void highlightSearchResults(SearchLocation location) {
// stub
}
@Override
public void clearHighlights() {
// stub
}
@Override
public SearchLocation search(String text, CursorPosition cursorPosition,
public SearchResults search(String text, CursorPosition cursorPosition,
boolean searchForward, boolean useRegex) {
return null;
}
@Override
public SearchResults searchAll(String text, boolean useRegex) {
return null;
}
@Override
public void dispose() {
// stub
}
}
}

View File

@@ -0,0 +1,400 @@
/* ###
* 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 static org.junit.Assert.*;
import java.util.*;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.swing.*;
import javax.swing.text.Highlighter.Highlight;
import javax.swing.text.StyledDocument;
import org.apache.commons.lang3.StringUtils;
import org.junit.Before;
import org.junit.Test;
import docking.test.AbstractDockingTest;
import docking.widgets.CursorPosition;
import docking.widgets.SearchLocation;
import ghidra.util.worker.Worker;
public class TextComponentSearcherTest extends AbstractDockingTest {
private JTextPane textPane = new JTextPane();
private TextComponentSearcher searcher = new TextComponentSearcher(textPane);
private int lineCount = 0;
private List<Line> expectedMatches = new ArrayList<>();
@Before
public void setUp() {
JScrollPane scrollPane = new JScrollPane(textPane);
JFrame frame = new JFrame("Find Dialog Test");
frame.setSize(400, 400);
frame.getContentPane().add(scrollPane);
frame.setVisible(true);
waitForSwing();
}
@Test
public void testFindNextPrevious_ChangeDocument() throws Exception {
// After changing the document, the results are stale and highlights should not work. But,
// the results should remain.
createDocumenText();
String searchText = "text";
TextComponentSearchResults results = searchNext(searchText);
assertFalse(isEmpty(results));
assertValid(results, searchText);
assertTrue(hasHighlights(results));
add("More text in the document after the search");
waitFor(results);
assertInvalid(results, searchText);
assertFalse(isEmpty(results));
assertFalse(hasHighlights(results));
// call search again and get new, valid results
TextComponentSearchResults newResults = searchNext(searchText);
assertNotEquals(results, newResults);
assertFalse(isEmpty(newResults));
assertValid(newResults, searchText);
assertTrue(hasHighlights(newResults));
}
@Test
public void testSearchAll_ChangeDocument() throws Exception {
// After changing the document, the results are stale and highlights should not work. But,
// the results should remain.
createDocumenText();
String searchText = "text";
TextComponentSearchResults results = searchAll(searchText);
assertFalse(isEmpty(results));
assertValid(results, searchText);
assertTrue(hasHighlights(results));
add("More text in the document after the search");
waitFor(results);
assertInvalid(results, searchText);
assertFalse(isEmpty(results));
assertFalse(hasHighlights(results));
// call search again and get new, valid results
TextComponentSearchResults newResults = searchAll(searchText);
assertNotEquals(results, newResults);
assertFalse(isEmpty(newResults));
assertValid(newResults, searchText);
assertTrue(hasHighlights(newResults));
}
@Test
public void testSearchAll_ContextTruncation_EvenLimit() throws Exception {
//
// Test that the context generated for the search results gets correctly truncated. We test
// various boundary conditions
//
int max = 20;
doTest(max);
}
@Test
public void testSearchAll_ContextTruncation_OddLimit() throws Exception {
int max = 21;
doTest(max);
}
@Test
public void testSearchAll_ManySearches_Activation() throws Exception {
//
// Test that we can activate and deactivate many search results rapidly and get the correct
// behavior. There is a worker queue managing activation requests. We hope to ensure the
// jobs are processed correctly.
//
createDocumenText();
// text
Map<String, SearchResults> allResults = new HashMap<>();
SearchResults results = searchAll("text");
allResults.put("text", results);
// some
results = searchAll("some");
allResults.put("some", results);
// leading
results = searchAll("leading");
allResults.put("leading", results);
// trailing
results = searchAll("trailing");
allResults.put("trailing", results);
Set<Entry<String, SearchResults>> entries = allResults.entrySet();
for (Entry<String, SearchResults> entry : entries) {
String searchText = entry.getKey();
TextComponentSearchResults searchResults =
(TextComponentSearchResults) entry.getValue();
searchResults.activate();
waitFor(searchResults);
assertTrue("Search results did not activate '%s'".formatted(searchText),
searchResults.isActive());
}
// Each call could possible cancel any activate request that has not finished. The last
// request should be active.
allResults.get("leading").activate();
allResults.get("trailing").activate();
allResults.get("leading").activate();
allResults.get("text").activate();
allResults.get("some").activate();
String searchText = "some";
TextComponentSearchResults lastResults =
(TextComponentSearchResults) allResults.get(searchText);
waitFor(lastResults);
assertTrue("Search results did not activate '%s'".formatted(searchText),
lastResults.isActive());
}
@Test
public void testSearchAll_ManySearches_Disposal() throws Exception {
//
// Test that we can activate and deactivate many search results rapidly and get the correct
// behavior. There is a worker queue managing activation requests. We hope to ensure the
// jobs are processed correctly.
//
createDocumenText();
TextComponentSearchResults results = searchAll("text");
assertTrue(results.isActive());
results.deactivate();
assertInactive(results);
results.activate();
assertActive(results);
results.dispose();
assertInactive(results);
// make sure active() does not work once disposed
results.activate();
assertDisposed(results);
}
private TextComponentSearchResults searchNext(String text) {
CursorPosition cursor = new CursorPosition(0);
SearchResults results = searcher.search(text, cursor, true, false);
waitFor(results);
return (TextComponentSearchResults) results;
}
private TextComponentSearchResults searchAll(String text) {
TextComponentSearchResults results = searcher.searchAll(text, false);
waitFor(results);
return results;
}
private void waitFor(SearchResults results) {
Worker worker = results.getWorker();
waitFor(() -> !worker.isBusy());
}
private void doTest(int max) throws Exception {
runSwing(() -> searcher.setMaxContextChars(max));
String searchText = "gold";
createDocumenText(searchText, max);
SearchResults results = searchAll(searchText);
assertMatches(results, max);
}
private void createDocumenText() throws Exception {
createDocumenText("stuff", 20);
}
private void createDocumenText(String searchText, int max) throws Exception {
add("No match on this line", false);
add("%s", searchText);
// non-truncated
add("We found %s here", searchText);
// truncated middle match
add("This text will be (%s) truncated, since it is more than max", searchText);
// truncated beginning match
add("%s, this text will be truncated, since it is more than max", searchText);
// truncated end match
add("This text will be truncated, since it is more than max, %s", searchText);
// truncated at beginning boundary
int half = (max - searchText.length()) / 2; // 'half' of the available space; max includes search text
add(padLeft(half, "%s with some trailing text that is quite long", searchText));
add(padLeft(half - 1, "%s with some trailing text that is quite long", searchText));
add(padLeft(half + 1, "%s with some trailing text that is quite long", searchText));
// truncated at end boundary
add(padRight(half, "this is some leading text that is quite long, %s", searchText));
add(padRight(half - 1, "this is some leading text that is quite long, %s", searchText));
add(padRight(half + 1, "this is some leading text that is quite long, %s", searchText));
// truncated at beginning at ellipses
add(padLeft(half, "%s with some trailing text that is quite long", searchText));
}
private void assertMatches(SearchResults results, int max) {
List<SearchLocation> locations = results.getLocations();
Map<Integer, SearchLocation> locationsByLine = locations.stream()
.collect(Collectors.toMap(loc -> loc.getLineNumber(), Function.identity()));
// Debug
// Set<Entry<Integer, SearchLocation>> entries1 = locationsByLine.entrySet();
// for (Entry<Integer, SearchLocation> entry : entries1) {
// SearchLocation loc = entry.getValue();
// Msg.debug(this, loc.getContext());
// }
for (Line line : expectedMatches) {
int n = line.lineNumber();
SearchLocation result = locationsByLine.remove(n);
assertNotNull("No match at line: " + n, result);
SearchLocationContext context = result.getContext();
String text = context.getPlainText();
int length = text.length();
int maxWithEllipses = max + 6; // ... text ...
assertTrue("Length is to long. Expected max %s, but found %s"
.formatted(maxWithEllipses, length),
length <= maxWithEllipses);
}
if (!locationsByLine.isEmpty()) {
StringBuilder sb = new StringBuilder();
sb.append("Found more search results than expected:").append('\n');
Set<Entry<Integer, SearchLocation>> entries = locationsByLine.entrySet();
for (Entry<Integer, SearchLocation> entry : entries) {
SearchLocation loc = entry.getValue();
sb.append(loc.toString()).append('\n');
}
fail(sb.toString());
}
}
private void assertValid(TextComponentSearchResults results, String searchText) {
boolean invalid = runSwing(() -> results.isInvalid(searchText));
assertFalse(invalid);
}
private void assertInvalid(TextComponentSearchResults results, String searchText) {
boolean invalid = runSwing(() -> results.isInvalid(searchText));
assertTrue(invalid);
}
private boolean isEmpty(TextComponentSearchResults results) {
return runSwing(() -> results.isEmpty());
}
private boolean hasHighlights(TextComponentSearchResults results) {
Highlight[] highlights = runSwing(() -> results.getHighlights());
return highlights.length > 0;
}
private void assertActive(TextComponentSearchResults results) {
waitFor(results);
assertTrue(results.isActive());
Highlight[] highlights = runSwing(() -> results.getHighlights());
List<SearchLocation> locations = results.getLocations();
assertEquals(locations.size(), highlights.length);
}
private void assertInactive(TextComponentSearchResults results) {
waitFor(results);
assertFalse(results.isActive());
Highlight[] highlights = runSwing(() -> results.getHighlights());
assertEquals(0, highlights.length);
}
private void assertDisposed(TextComponentSearchResults results) {
waitFor(results);
assertFalse(results.isActive());
Highlight[] highlights = runSwing(() -> results.getHighlights());
assertEquals(0, highlights.length);
List<SearchLocation> locations = results.getLocations();
assertEquals(0, locations.size());
}
private String padLeft(int n, String s, String searchText) {
s = s.formatted(searchText);
String pad = StringUtils.repeat('@', n);
return pad + s;
}
private String padRight(int n, String s, String searchText) {
s = s.formatted(searchText);
String pad = StringUtils.repeat('@', n);
return s + pad;
}
private void add(String s) throws Exception {
add(s, true);
}
private void add(String raw, String searchText) throws Exception {
String s = raw;
boolean hasMatch = searchText != null;
if (hasMatch) {
s = raw.formatted(searchText);
}
add(s, true);
}
private void add(String s, boolean hasMatch) throws Exception {
runSwingWithException(() -> {
StyledDocument sd = textPane.getStyledDocument();
sd.insertString(sd.getLength(), s + '\n', null);
});
lineCount++;
if (!hasMatch) {
return;
}
Line line = new Line(s, lineCount);
expectedMatches.add(line);
}
private record Line(String text, int lineNumber) {}
}

View File

@@ -18,6 +18,8 @@ package generic.concurrent;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import ghidra.util.task.CancelledListener;
import ghidra.util.task.TaskMonitor;
@@ -487,29 +489,65 @@ public class ConcurrentQ<I, R> {
* @return a list of all items that have not yet been queued to the threadPool.
*/
public List<I> cancelAllTasks(boolean interruptRunningTasks) {
List<FutureTaskMonitor<I, R>> tasksToBeCancelled = new ArrayList<>();
return cancelAllTasks(i -> true, interruptRunningTasks);
}
/**
* Cancels the processing of currently scheduled items in this queue that match the given
* predicate. Any items that haven't yet been scheduled on the threadPool are returned
* immediately from this call. Items that are currently being processed will be cancelled and
* those results will be available on the next waitForResults() call and also if there is a
* QItemListener, it will be called with the QResult. There is no guarantee that scheduled
* tasks will terminate any time soon. If they check the isCancelled() state of their QMonitor,
* it will be true. Setting the interruptRunningTasks to true, will result in a thread
* interrupt to any currently running task which might be useful if the task perform waiting
* operations like I/O.
*
* @param p the predicate that signals which jobs to cancel
* @param interruptRunningTasks if true, an attempt will be made to interrupt any currently
* processing thread.
* @return a list of all items that have not yet been queued to the threadPool.
*/
public List<I> cancelAllTasks(Predicate<I> p, boolean interruptRunningTasks) {
List<FutureTaskMonitor<I, R>> tasksToCancel;
List<I> nonStartedItems;
lock.lock();
try {
nonStartedItems = removeUnscheduledJobs();
tasksToBeCancelled.addAll(taskSet);
nonStartedItems = removeUnscheduledJobs(p);
tasksToCancel = taskSet.stream()
.filter(t -> p.test(t.getItem()))
.collect(Collectors.toList());
}
finally {
lock.unlock();
}
for (FutureTaskMonitor<I, R> task : tasksToBeCancelled) {
for (FutureTaskMonitor<I, R> task : tasksToCancel) {
task.cancel(interruptRunningTasks);
}
return nonStartedItems;
}
/**
* Removes all unscheduled jobs
* @return the removed jobs
*/
public List<I> removeUnscheduledJobs() {
return removeUnscheduledJobs(i -> true);
}
/**
* Removes all unscheduled jobs matching the given predicate
* @param p the predicate
* @return the removed jobs
*/
public List<I> removeUnscheduledJobs(Predicate<I> p) {
List<I> nonStartedItems = new ArrayList<>();
lock.lock();
try {
tracker.neverStartedItemsRemoved(queue.size());
nonStartedItems.addAll(queue);
queue.clear();
nonStartedItems = queue.stream().filter(p).collect(Collectors.toList());
tracker.neverStartedItemsRemoved(nonStartedItems.size());
queue.removeAll(nonStartedItems);
}
finally {
lock.unlock();

View File

@@ -4,9 +4,9 @@
* 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.
@@ -19,6 +19,7 @@ import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;
import generic.concurrent.*;
import ghidra.util.Msg;
@@ -187,9 +188,8 @@ public abstract class AbstractWorker<T extends Job> {
}
/**
* Schedules the job for execution. Jobs will be processed in priority order. The
* highest priority jobs are those with the lowest value return by the job's getPriority()
* method. (i.e. the job with priority 0 will be processed before the job with priority 1)
* Schedules the job for execution. Jobs will be processed according to the queue supplied at
* construction time (e.g., in priority order or 1 at a time).
* @param job the job to be executed.
*/
public void schedule(T job) {
@@ -206,7 +206,22 @@ public abstract class AbstractWorker<T extends Job> {
* Clears any pending jobs and cancels any currently executing job.
*/
public void clearAllJobs() {
clearAllJobs(false);
clearAllJobs(t -> true);
}
/**
* Clears any pending jobs and currently executing jobs that match the given predicate.
* @param p the predicate
*/
public void clearAllJobs(Predicate<T> p) {
doClearAllJobs(p, false);
}
private void doClearAllJobs(Predicate<T> p, boolean interruptRunninJob) {
List<T> pendingJobs = concurrentQ.cancelAllTasks(p, interruptRunninJob);
for (T job : pendingJobs) {
job.cancel();
}
}
/**
@@ -222,14 +237,7 @@ public abstract class AbstractWorker<T extends Job> {
* </b>
*/
public void clearAllJobsWithInterrupt_IKnowTheRisks() {
clearAllJobs(true);
}
private void clearAllJobs(boolean interruptRuningJob) {
List<T> pendingJobs = concurrentQ.cancelAllTasks(interruptRuningJob);
for (T job : pendingJobs) {
job.cancel();
}
doClearAllJobs(t -> true, true);
}
/**

View File

@@ -4,9 +4,9 @@
* 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.
@@ -15,14 +15,16 @@
*/
package ghidra.util.worker;
import ghidra.util.task.TaskMonitor;
import java.util.Comparator;
import java.util.concurrent.PriorityBlockingQueue;
import ghidra.util.task.TaskMonitor;
/**
* Executes a single job at a time in priority order.
*
* <p>
* The highest priority jobs are those with the lowest value return by the job's getPriority()
* method. (i.e. the job with priority 0 will be processed before the job with priority 1)
* @see Worker
*/
public class PriorityWorker extends AbstractWorker<PriorityJob> {

View File

@@ -84,5 +84,4 @@ public class Worker extends AbstractWorker<Job> {
super(new LinkedBlockingQueue<Job>(), isPersistentThread, name, useSharedThreadPool,
monitor);
}
}

View File

@@ -75,6 +75,7 @@ color.palette.aliceblue = rgb(45,47,65) // dark blue gray
color.palette.black = darkgray
color.palette.blue = lightskyblue
color.palette.blueviolet = violet
color.palette.cornflowerblue = rgb(61,91,145) // less bright cornflowerblue
color.palette.crimson = lightcoral
color.palette.cyan = cadetblue // not sure; can change
color.palette.darkgray = dimgray