From 2ae42befb809444966a9a37a2d6e72fbb61d65ea Mon Sep 17 00:00:00 2001
From: dragonmacher <48328597+dragonmacher@users.noreply.github.com>
Date: Wed, 12 Nov 2025 12:34:30 -0500
Subject: [PATCH] GP-6076 - Find All - Added a button to the Find dialog to
find all matches and show the results in a table
---
Ghidra/Features/Base/certification.manifest | 1 -
.../Base/data/base.icons.theme.properties | 1 -
.../FrontEndPlugin/Ghidra_Front_end.htm | 2 +-
.../console/ConsoleComponentProvider.java | 11 +-
.../InterpreterComponentProvider.java | 13 +-
.../locationreferences/LocationReference.java | 17 +-
.../LocationReferencesPlugin.java | 2 +-
.../LocationReferencesProvider.java | 2 +-
.../LocationReferencesTableModel.java | 9 +-
.../locationreferences/ReferenceUtils.java | 3 +-
.../core/scalartable/ScalarSearchPlugin.java | 6 +-
.../scalartable/ScalarSearchProvider.java | 2 +-
.../core/table/TableComponentProvider.java | 2 +-
.../plugin/core/table/TableServicePlugin.java | 14 +-
.../app/services/DataTypeReference.java | 12 +-
.../memsearch/gui/MemorySearchProvider.java | 2 +-
.../base/quickfix/QuickFixTableProvider.java | 2 +-
.../java/ghidra/util/table/GhidraTable.java | 2 +
.../core/console/ConsolePluginTest.java | 172 ++++-
.../DecompilerDiffViewFindAction.java | 11 +-
.../data/decompiler.theme.properties | 3 +-
.../app/decompiler/DecompileOptions.java | 8 +
.../component/DecompilerFindDialog.java | 237 +------
.../decompiler/component/DecompilerPanel.java | 67 +-
.../actions/DecompilerCursorPosition.java | 10 +-
.../actions/DecompilerSearchLocation.java | 25 +-
.../actions/DecompilerSearchResults.java | 245 +++++++
.../decompile/actions/DecompilerSearcher.java | 233 +++----
.../core/decompile/actions/FindAction.java | 5 +
.../decompile/DecompilerFindDialogTest.java | 6 +-
.../datatype/finder/DecompilerReference.java | 12 +-
.../datatype/finder/VariableAccessDR.java | 10 +-
.../extension/datatype/finder/VariableDR.java | 8 +-
.../core/search/DecompilerTextFinder.java | 14 +-
.../DecompilerTextFinderTableModel.java | 14 +-
.../app/plugin/core/search/TextMatch.java | 8 +-
.../Framework/Docking/certification.manifest | 1 +
.../Docking/data/docking.theme.properties | 1 +
.../src/main/java/docking/ComponentNode.java | 10 +-
.../java/docking/ComponentPlaceholder.java | 1 +
.../main/java/docking/ComponentProvider.java | 2 +-
.../java/docking/help/HelpViewSearcher.java | 188 +++++-
.../main/java/docking/widgets/FindDialog.java | 121 +++-
.../widgets/FindDialogResultsProvider.java | 342 ++++++++++
.../java/docking/widgets/SearchLocation.java | 52 +-
.../widgets/TextComponentSearcher.java | 539 ---------------
.../{ => search}/FindDialogSearcher.java | 46 +-
.../search/SearchLocationContext.java} | 27 +-
.../search/SearchLocationContextBuilder.java} | 22 +-
.../docking/widgets/search/SearchResults.java | 302 +++++++++
.../search/TextComponentSearchLocation.java | 47 ++
.../search/TextComponentSearchResults.java | 616 ++++++++++++++++++
.../widgets/search/TextComponentSearcher.java | 335 ++++++++++
.../tabbedpane/DockingTabRenderer.java | 16 +-
.../table/actions/DeleteTableRowAction.java | 15 +-
.../main/resources/images/table_delete.png | Bin
.../java/docking/widgets/FindDialogTest.java | 28 +-
.../search/TextComponentSearcherTest.java | 400 ++++++++++++
.../java/generic/concurrent/ConcurrentQ.java | 52 +-
.../ghidra/util/worker/AbstractWorker.java | 36 +-
.../ghidra/util/worker/PriorityWorker.java | 12 +-
.../main/java/ghidra/util/worker/Worker.java | 1 -
.../Gui/data/gui.palette.theme.properties | 1 +
63 files changed, 3131 insertions(+), 1273 deletions(-)
create mode 100644 Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/DecompilerSearchResults.java
create mode 100644 Ghidra/Framework/Docking/src/main/java/docking/widgets/FindDialogResultsProvider.java
delete mode 100644 Ghidra/Framework/Docking/src/main/java/docking/widgets/TextComponentSearcher.java
rename Ghidra/Framework/Docking/src/main/java/docking/widgets/{ => search}/FindDialogSearcher.java (68%)
rename Ghidra/{Features/Base/src/main/java/ghidra/app/plugin/core/navigation/locationreferences/LocationReferenceContext.java => Framework/Docking/src/main/java/docking/widgets/search/SearchLocationContext.java} (85%)
rename Ghidra/{Features/Base/src/main/java/ghidra/app/plugin/core/navigation/locationreferences/LocationReferenceContextBuilder.java => Framework/Docking/src/main/java/docking/widgets/search/SearchLocationContextBuilder.java} (72%)
create mode 100644 Ghidra/Framework/Docking/src/main/java/docking/widgets/search/SearchResults.java
create mode 100644 Ghidra/Framework/Docking/src/main/java/docking/widgets/search/TextComponentSearchLocation.java
create mode 100644 Ghidra/Framework/Docking/src/main/java/docking/widgets/search/TextComponentSearchResults.java
create mode 100644 Ghidra/Framework/Docking/src/main/java/docking/widgets/search/TextComponentSearcher.java
rename Ghidra/{Features/Base/src/main/java/ghidra/util => Framework/Docking/src/main/java/docking/widgets}/table/actions/DeleteTableRowAction.java (93%)
rename Ghidra/{Features/Base => Framework/Docking}/src/main/resources/images/table_delete.png (100%)
create mode 100644 Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/search/TextComponentSearcherTest.java
diff --git a/Ghidra/Features/Base/certification.manifest b/Ghidra/Features/Base/certification.manifest
index 3463cb92a8..646decbb0d 100644
--- a/Ghidra/Features/Base/certification.manifest
+++ b/Ghidra/Features/Base/certification.manifest
@@ -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|
diff --git a/Ghidra/Features/Base/data/base.icons.theme.properties b/Ghidra/Features/Base/data/base.icons.theme.properties
index 85b4e71297..98e7689a3a 100644
--- a/Ghidra/Features/Base/data/base.icons.theme.properties
+++ b/Ghidra/Features/Base/data/base.icons.theme.properties
@@ -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
diff --git a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/Ghidra_Front_end.htm b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/Ghidra_Front_end.htm
index 6a3ffc3641..12b50fd46b 100644
--- a/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/Ghidra_Front_end.htm
+++ b/Ghidra/Features/Base/src/main/help/help/topics/FrontEndPlugin/Ghidra_Front_end.htm
@@ -108,7 +108,7 @@
-The data tree shows all files in the project orgnanized into folders and sub-folders. +
The data tree shows all files in the project organized into folders and sub-folders. Icons for files indicate whether they are under version control and whether you have the file 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 } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/locationreferences/LocationReference.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/locationreferences/LocationReference.java index bd6a8bf1bd..4edc7dd3e4 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/locationreferences/LocationReference.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/locationreferences/LocationReference.java @@ -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
{ 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 { // 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(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 { * * @return the context */ - public LocationReferenceContext getContext() { + public SearchLocationContext getContext() { return context; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/locationreferences/LocationReferencesPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/locationreferences/LocationReferencesPlugin.java index 10bd4a9bd4..7c337f2eb9 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/locationreferences/LocationReferencesPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/locationreferences/LocationReferencesPlugin.java @@ -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. diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/locationreferences/LocationReferencesProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/locationreferences/LocationReferencesProvider.java index 2685f1eeb1..d62fd286e5 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/locationreferences/LocationReferencesProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/locationreferences/LocationReferencesProvider.java @@ -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; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/locationreferences/LocationReferencesTableModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/locationreferences/LocationReferencesTableModel.java index 5736acd5ec..f93a71f6bf 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/locationreferences/LocationReferencesTableModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/navigation/locationreferences/LocationReferencesTableModel.java @@ -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 callback = ref -> { - LocationReferenceContext context = ref.getContext(); + SearchLocationContext context = ref.getContext(); LocationReference locationReference = new LocationReference(ref.getAddress(), context); accumulator.add(locationReference); }; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/scalartable/ScalarSearchPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/scalartable/ScalarSearchPlugin.java index 4c1b7b9a31..347f72f1c9 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/scalartable/ScalarSearchPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/scalartable/ScalarSearchPlugin.java @@ -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; /** diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/scalartable/ScalarSearchProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/scalartable/ScalarSearchProvider.java index 320ac43882..5dfbf74666 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/scalartable/ScalarSearchProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/scalartable/ScalarSearchProvider.java @@ -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; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/table/TableComponentProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/table/TableComponentProvider.java index 59be2c2edd..709b17f2a1 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/table/TableComponentProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/table/TableComponentProvider.java @@ -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; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/table/TableServicePlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/table/TableServicePlugin.java index f58bceb15c..bdf64cfa60 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/table/TableServicePlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/table/TableServicePlugin.java @@ -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 iter = programMap.keySet().iterator(); - while (iter.hasNext()) { - Program p = iter.next(); + for (Program p : programMap.keySet()) { List > 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 iter = programToDialogMap.keySet().iterator(); - while (iter.hasNext()) { - Program p = iter.next(); + for (Program p : programToDialogMap.keySet()) { List list = programToDialogMap.get(p); if (list.remove(dialog)) { if (list.size() == 0) { @@ -213,9 +209,7 @@ public class TableServicePlugin extends ProgramPlugin private List > getProviders() { List > clist = new ArrayList<>(); - Iterator >> iter = programMap.values().iterator(); - while (iter.hasNext()) { - List
> list = iter.next(); + for (List > list : programMap.values()) { clist.addAll(list); } return clist; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/services/DataTypeReference.java b/Ghidra/Features/Base/src/main/java/ghidra/app/services/DataTypeReference.java index 764e2ccd2b..96b8d980b1 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/services/DataTypeReference.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/services/DataTypeReference.java @@ -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; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemorySearchProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemorySearchProvider.java index a572739c9b..972e0d48de 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemorySearchProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemorySearchProvider.java @@ -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; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/QuickFixTableProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/QuickFixTableProvider.java index 5f6c966b36..66d97cd89e 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/QuickFixTableProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/quickfix/QuickFixTableProvider.java @@ -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; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/util/table/GhidraTable.java b/Ghidra/Features/Base/src/main/java/ghidra/util/table/GhidraTable.java index 22059abe04..c86e8ccebd 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/util/table/GhidraTable.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/util/table/GhidraTable.java @@ -281,6 +281,8 @@ public class GhidraTable extends GTable { * * This method differs from {@link #navigate(int, int)} in that this method will not navigate if * {@link #navigateOnSelection} is
false. + * @param row the row + * @param column the column */ protected void navigateOnCurrentSelection(int row, int column) { if (!navigateOnSelection) { diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/console/ConsolePluginTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/console/ConsolePluginTest.java index e7a48de523..04dbf57fc5 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/console/ConsolePluginTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/plugin/core/console/ConsolePluginTest.java @@ -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()); + Listmatches = getExpectedMatches(searchText); + assertEquals(3, matches.size()); + + verfiyHighlightColor(matches); + + FindDialogResultsProvider resultsProvider = + waitForComponentProvider(FindDialogResultsProvider.class); + List 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 matches = getMatches(); + String searchText = "Hello"; + find(searchText); + + List 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 matches = getMatches(); + String searchText = "Hello"; + find(searchText); + + List 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 matches = getMatches(); + String searchText = "Hello"; + find(searchText); + + List 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 matches = getMatches(); + String searchText = "Hello"; + find(searchText); + + List 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 matches = getMatches(); + String searchText = "Hello"; + find(searchText); + + List 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 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 matches = getMatches(); + String searchText = "Hello"; + find(searchText); + + List 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 matches = getMatches(); + String searchText = "Hello"; + find(searchText); + + List 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 matches = getMatches(); + String searchText = "Hello"; + find(searchText); + + List 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 matches = getMatches(); + String searchText = "Hello"; + find(searchText); + + List 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 matches = getMatches(); + String searchText = "Hello"; + find(searchText); + + List 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 matches) + private void selectRow(FindDialogResultsProvider resultsProvider, int row) { + GTable table = resultsProvider.getTable(); + runSwing(() -> table.selectRow(row)); + } + + private void verfiyHighlightColor(List 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 getMatches() { + private List getExpectedMatches() { String searchText = findDialog.getSearchText(); + assertFalse(searchText.isEmpty()); + return getExpectedMatches(searchText); + } + + private List getExpectedMatches(String searchText) { List 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() { diff --git a/Ghidra/Features/CodeCompare/src/main/java/ghidra/features/codecompare/decompile/DecompilerDiffViewFindAction.java b/Ghidra/Features/CodeCompare/src/main/java/ghidra/features/codecompare/decompile/DecompilerDiffViewFindAction.java index b721cdb47d..a37d53c14a 100644 --- a/Ghidra/Features/CodeCompare/src/main/java/ghidra/features/codecompare/decompile/DecompilerDiffViewFindAction.java +++ b/Ghidra/Features/CodeCompare/src/main/java/ghidra/features/codecompare/decompile/DecompilerDiffViewFindAction.java @@ -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; } diff --git a/Ghidra/Features/Decompiler/data/decompiler.theme.properties b/Ghidra/Features/Decompiler/data/decompiler.theme.properties index 816d20be6f..62635b64fd 100644 --- a/Ghidra/Features/Decompiler/data/decompiler.theme.properties +++ b/Ghidra/Features/Decompiler/data/decompiler.theme.properties @@ -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 diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/DecompileOptions.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/DecompileOptions.java index 55453956c7..53abe316d3 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/DecompileOptions.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/DecompileOptions.java @@ -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 */ diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerFindDialog.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerFindDialog.java index 52497d160e..0643d74f30 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerFindDialog.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerFindDialog.java @@ -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 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 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 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 { - - private List searchLocations; - - DecompilerFindResultsModel(ServiceProvider sp, Program program, - List searchLocations) { - super("Decompiler Search All Results", sp, program, null); - this.searchLocations = searchLocations.stream() - .map(l -> (DecompilerSearchLocation) l) - .collect(Collectors.toList()); - } - - @Override - protected TableColumnDescriptor createTableColumnDescriptor() { - - TableColumnDescriptor descriptor = - new TableColumnDescriptor<>(); - descriptor.addVisibleColumn(new LineNumberColumn(), 1, true); - descriptor.addVisibleColumn(new ContextColumn()); - return descriptor; - } - - @Override - protected void doLoad(Accumulator 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 { - - @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 { - - private GColumnRenderer 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 getColumnRenderer() { - return renderer; - } - - private class ContextCellRenderer - extends AbstractGhidraColumnRenderer { - - { - // 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(); - } - } - } } } diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerPanel.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerPanel.java index 18ba63f001..0f5011f2dc 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerPanel.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerPanel.java @@ -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 > locationsByLine = + currentSearchResults.getLocationsByLine(); + List 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 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); } } diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/DecompilerCursorPosition.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/DecompilerCursorPosition.java index 98be2265b6..4cfaa021d7 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/DecompilerCursorPosition.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/DecompilerCursorPosition.java @@ -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(); + } } diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/DecompilerSearchLocation.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/DecompilerSearchLocation.java index 600835d6c4..8ac6ad901a 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/DecompilerSearchLocation.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/DecompilerSearchLocation.java @@ -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); + } } diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/DecompilerSearchResults.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/DecompilerSearchResults.java new file mode 100644 index 0000000000..812b0fcdfd --- /dev/null +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/DecompilerSearchResults.java @@ -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 searchLocations; + private Map > locationsByLine; + private TreeMap matchesByPosition = new TreeMap<>(); + + private DecompilerSearchLocation activeLocation; + + DecompilerSearchResults(Worker worker, DecompilerPanel decompilerPanel, String searchText, + List 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 getLocations() { + return searchLocations; + } + + public Map > 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 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 { + + @Override + public int compareTo(LinePosition other) { + + int result = line - other.line; + if (result != 0) { + return result; + } + + return col - other.col; + } + } + +} diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/DecompilerSearcher.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/DecompilerSearcher.java index ba6dcbdcb1..6850f65f36 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/DecompilerSearcher.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/DecompilerSearcher.java @@ -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 searchAll(String searchString, boolean isRegex) { - - Pattern pattern = createPattern(searchString, isRegex); - Function function = createForwardMatchFunction(pattern); + Pattern pattern = createPattern(searchText, isRegex); + Function forwardMatcher = createForwardMatchFunction(pattern); FieldLocation start = new FieldLocation(); List 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 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 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 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 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 matcher, - String searchString, FieldLocation currentLocation) { - - List 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) {} } diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/FindAction.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/FindAction.java index 5f7e390a96..3e845c0b87 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/FindAction.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/actions/FindAction.java @@ -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); } diff --git a/Ghidra/Features/Decompiler/src/test.slow/java/ghidra/app/plugin/core/decompile/DecompilerFindDialogTest.java b/Ghidra/Features/Decompiler/src/test.slow/java/ghidra/app/plugin/core/decompile/DecompilerFindDialogTest.java index 4735cd56ba..8ec79f64d4 100644 --- a/Ghidra/Features/Decompiler/src/test.slow/java/ghidra/app/plugin/core/decompile/DecompilerFindDialogTest.java +++ b/Ghidra/Features/Decompiler/src/test.slow/java/ghidra/app/plugin/core/decompile/DecompilerFindDialogTest.java @@ -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); diff --git a/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/extension/datatype/finder/DecompilerReference.java b/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/extension/datatype/finder/DecompilerReference.java index fc383c6f8f..5f1a3529b9 100644 --- a/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/extension/datatype/finder/DecompilerReference.java +++ b/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/extension/datatype/finder/DecompilerReference.java @@ -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 tokens = line.getAllTokens(); for (ClangToken token : tokens) { diff --git a/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/extension/datatype/finder/VariableAccessDR.java b/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/extension/datatype/finder/VariableAccessDR.java index a2ba9307a9..3982b995da 100644 --- a/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/extension/datatype/finder/VariableAccessDR.java +++ b/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/extension/datatype/finder/VariableAccessDR.java @@ -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; } diff --git a/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/extension/datatype/finder/VariableDR.java b/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/extension/datatype/finder/VariableDR.java index ff6825dedd..6cffe1f212 100644 --- a/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/extension/datatype/finder/VariableDR.java +++ b/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/extension/datatype/finder/VariableDR.java @@ -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)); } } diff --git a/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/plugin/core/search/DecompilerTextFinder.java b/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/plugin/core/search/DecompilerTextFinder.java index 38a0ade391..a212e056c2 100644 --- a/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/plugin/core/search/DecompilerTextFinder.java +++ b/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/plugin/core/search/DecompilerTextFinder.java @@ -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 matches) { + private SearchLocationContext createMatchContext(List 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); diff --git a/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/plugin/core/search/DecompilerTextFinderTableModel.java b/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/plugin/core/search/DecompilerTextFinderTableModel.java index c8b068265d..1b9892fa2a 100644 --- a/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/plugin/core/search/DecompilerTextFinderTableModel.java +++ b/Ghidra/Features/DecompilerDependent/src/main/java/ghidra/app/plugin/core/search/DecompilerTextFinderTableModel.java @@ -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 { + extends AbstractProgramBasedDynamicTableColumn { 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 getColumnRenderer() { + public GColumnRenderer getColumnRenderer() { return renderer; } } private class ContextCellRenderer - extends AbstractGhidraColumnRenderer { + extends AbstractGhidraColumnRenderer { { // the context uses html @@ -178,7 +178,7 @@ public class DecompilerTextFinderTableModel extends GhidraProgramTableModel { - 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 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 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 { // diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/FindDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/FindDialog.java index 6c06f0bcab..4d3dc86ebc 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/FindDialog.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/FindDialog.java @@ -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 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); } + } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/FindDialogResultsProvider.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/FindDialogResultsProvider.java new file mode 100644 index 0000000000..2644dd8b12 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/FindDialogResultsProvider.java @@ -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 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