mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2026-01-08 21:47:59 -05:00
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:
@@ -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|
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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|
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -448,6 +448,7 @@ public class ComponentPlaceholder {
|
||||
* @param newProvider the new provider
|
||||
*/
|
||||
void setProvider(ComponentProvider newProvider) {
|
||||
|
||||
this.componentProvider = newProvider;
|
||||
actions.clear();
|
||||
if (newProvider != null) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
//
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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() {
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 + "]");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
Before Width: | Height: | Size: 660 B After Width: | Height: | Size: 660 B |
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -84,5 +84,4 @@ public class Worker extends AbstractWorker<Job> {
|
||||
super(new LinkedBlockingQueue<Job>(), isPersistentThread, name, useSharedThreadPool,
|
||||
monitor);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user