mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2026-01-09 22:17:55 -05:00
GP-6076 - Find All - Added a button to the Find dialog to find all matches and show the results in a table
This commit is contained in:
@@ -1054,7 +1054,6 @@ src/main/resources/images/small_minus.png||GHIDRA||||END|
|
|||||||
src/main/resources/images/small_plus.png||GHIDRA||||END|
|
src/main/resources/images/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/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/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_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/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|
|
src/main/resources/images/tag_blue.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
|
||||||
|
|||||||
@@ -324,7 +324,6 @@ icon.plugin.symboltable.referencetable.provider = table_go.png
|
|||||||
|
|
||||||
icon.plugin.table.service = icon.search
|
icon.plugin.table.service = icon.search
|
||||||
icon.plugin.table.service.marker = icon.base.search.marker
|
icon.plugin.table.service.marker = icon.base.search.marker
|
||||||
icon.plugin.table.delete.row = table_delete.png
|
|
||||||
|
|
||||||
icon.plugin.totd.provider = help-hint.png
|
icon.plugin.totd.provider = help-hint.png
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,7 @@
|
|||||||
<IMG src= "images/ProjectDataTree.png" border="0">
|
<IMG src= "images/ProjectDataTree.png" border="0">
|
||||||
</CENTER>
|
</CENTER>
|
||||||
<BLOCKQUOTE>
|
<BLOCKQUOTE>
|
||||||
<P>The data tree shows all files in the project orgnanized into folders and sub-folders.
|
<P>The data tree shows all files in the project organized into folders and sub-folders.
|
||||||
<A href="#FileIcons">Icons for files</A> indicate whether they are under <A href=
|
<A href="#FileIcons">Icons for files</A> indicate whether they are under <A href=
|
||||||
"help/topics/VersionControl/project_repository.htm#Versioning">version control</A> and whether
|
"help/topics/VersionControl/project_repository.htm#Versioning">version control</A> and whether
|
||||||
you have the file <A href=
|
you have the file <A href=
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import docking.*;
|
|||||||
import docking.action.*;
|
import docking.action.*;
|
||||||
import docking.action.builder.ActionBuilder;
|
import docking.action.builder.ActionBuilder;
|
||||||
import docking.widgets.FindDialog;
|
import docking.widgets.FindDialog;
|
||||||
import docking.widgets.TextComponentSearcher;
|
import docking.widgets.search.TextComponentSearcher;
|
||||||
import generic.theme.GIcon;
|
import generic.theme.GIcon;
|
||||||
import generic.theme.Gui;
|
import generic.theme.Gui;
|
||||||
import ghidra.app.script.DecoratingPrintWriter;
|
import ghidra.app.script.DecoratingPrintWriter;
|
||||||
@@ -69,7 +69,6 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
|
|||||||
private Address currentAddress;
|
private Address currentAddress;
|
||||||
|
|
||||||
private FindDialog findDialog;
|
private FindDialog findDialog;
|
||||||
private TextComponentSearcher searcher;
|
|
||||||
|
|
||||||
public ConsoleComponentProvider(PluginTool tool, String owner) {
|
public ConsoleComponentProvider(PluginTool tool, String owner) {
|
||||||
super(tool, "Console", owner);
|
super(tool, "Console", owner);
|
||||||
@@ -192,8 +191,8 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
|
|||||||
|
|
||||||
private void showFindDialog() {
|
private void showFindDialog() {
|
||||||
if (findDialog == null) {
|
if (findDialog == null) {
|
||||||
searcher = new TextComponentSearcher(textPane);
|
TextComponentSearcher searcher = new TextComponentSearcher(textPane);
|
||||||
findDialog = new FindDialog("Find", searcher);
|
findDialog = new FindDialog("Console Find", searcher);
|
||||||
}
|
}
|
||||||
getTool().showDialog(findDialog);
|
getTool().showDialog(findDialog);
|
||||||
}
|
}
|
||||||
@@ -220,8 +219,8 @@ public class ConsoleComponentProvider extends ComponentProviderAdapter implement
|
|||||||
checkVisible();
|
checkVisible();
|
||||||
textPane.setText("");
|
textPane.setText("");
|
||||||
|
|
||||||
if (searcher != null) {
|
if (findDialog != null) {
|
||||||
searcher.clearHighlights();
|
findDialog.close(); // this will also dispose of any search highlights
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ import docking.ActionContext;
|
|||||||
import docking.action.DockingAction;
|
import docking.action.DockingAction;
|
||||||
import docking.action.ToolBarData;
|
import docking.action.ToolBarData;
|
||||||
import docking.action.builder.ActionBuilder;
|
import docking.action.builder.ActionBuilder;
|
||||||
import docking.widgets.*;
|
import docking.widgets.FindDialog;
|
||||||
|
import docking.widgets.OptionDialog;
|
||||||
|
import docking.widgets.search.TextComponentSearcher;
|
||||||
import generic.theme.GIcon;
|
import generic.theme.GIcon;
|
||||||
import ghidra.app.util.HelpTopics;
|
import ghidra.app.util.HelpTopics;
|
||||||
import ghidra.framework.plugintool.ComponentProviderAdapter;
|
import ghidra.framework.plugintool.ComponentProviderAdapter;
|
||||||
@@ -41,7 +43,6 @@ public class InterpreterComponentProvider extends ComponentProviderAdapter
|
|||||||
private List<Callback> firstActivationCallbacks;
|
private List<Callback> firstActivationCallbacks;
|
||||||
|
|
||||||
private FindDialog findDialog;
|
private FindDialog findDialog;
|
||||||
private TextComponentSearcher searcher;
|
|
||||||
|
|
||||||
public InterpreterComponentProvider(InterpreterPanelPlugin plugin,
|
public InterpreterComponentProvider(InterpreterPanelPlugin plugin,
|
||||||
InterpreterConnection interpreter, boolean visible) {
|
InterpreterConnection interpreter, boolean visible) {
|
||||||
@@ -95,8 +96,8 @@ public class InterpreterComponentProvider extends ComponentProviderAdapter
|
|||||||
private void showFindDialog() {
|
private void showFindDialog() {
|
||||||
if (findDialog == null) {
|
if (findDialog == null) {
|
||||||
JTextPane textPane = panel.getOutputTextPane();
|
JTextPane textPane = panel.getOutputTextPane();
|
||||||
searcher = new TextComponentSearcher(textPane);
|
TextComponentSearcher searcher = new TextComponentSearcher(textPane);
|
||||||
findDialog = new FindDialog("Find", searcher);
|
findDialog = new FindDialog("Intepreter Find", searcher);
|
||||||
}
|
}
|
||||||
getTool().showDialog(findDialog);
|
getTool().showDialog(findDialog);
|
||||||
}
|
}
|
||||||
@@ -154,8 +155,8 @@ public class InterpreterComponentProvider extends ComponentProviderAdapter
|
|||||||
public void clear() {
|
public void clear() {
|
||||||
panel.clear();
|
panel.clear();
|
||||||
|
|
||||||
if (searcher != null) {
|
if (findDialog != null) {
|
||||||
searcher.clearHighlights();
|
findDialog.close(); // this will also dispose of any search highlights
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,11 @@
|
|||||||
*/
|
*/
|
||||||
package ghidra.app.plugin.core.navigation.locationreferences;
|
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 java.util.Objects;
|
||||||
|
|
||||||
|
import docking.widgets.search.SearchLocationContext;
|
||||||
import ghidra.program.model.address.Address;
|
import ghidra.program.model.address.Address;
|
||||||
import ghidra.program.model.symbol.DynamicReference;
|
import ghidra.program.model.symbol.DynamicReference;
|
||||||
import ghidra.program.model.symbol.Reference;
|
import ghidra.program.model.symbol.Reference;
|
||||||
@@ -34,7 +35,7 @@ public class LocationReference implements Comparable<LocationReference> {
|
|||||||
private final boolean isOffcutReference;
|
private final boolean isOffcutReference;
|
||||||
private final Address locationOfUseAddress;
|
private final Address locationOfUseAddress;
|
||||||
private final String refType;
|
private final String refType;
|
||||||
private final LocationReferenceContext context;
|
private final SearchLocationContext context;
|
||||||
private final ProgramLocation location;
|
private final ProgramLocation location;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,7 +53,7 @@ public class LocationReference implements Comparable<LocationReference> {
|
|||||||
// represents the 'from' address for a reference; for parameters and variables of a
|
// represents the 'from' address for a reference; for parameters and variables of a
|
||||||
// function, this represents the address of that variable.
|
// function, this represents the address of that variable.
|
||||||
private LocationReference(Address address, ProgramLocation location, String refType,
|
private LocationReference(Address address, ProgramLocation location, String refType,
|
||||||
LocationReferenceContext context, boolean isOffcut) {
|
SearchLocationContext context, boolean isOffcut) {
|
||||||
this.locationOfUseAddress = Objects.requireNonNull(address);
|
this.locationOfUseAddress = Objects.requireNonNull(address);
|
||||||
this.location = location;
|
this.location = location;
|
||||||
this.refType = refType == null ? "" : refType;
|
this.refType = refType == null ? "" : refType;
|
||||||
@@ -75,15 +76,15 @@ public class LocationReference implements Comparable<LocationReference> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LocationReference(Address locationOfUseAddress, String context) {
|
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) {
|
LocationReference(Address locationOfUseAddress, SearchLocationContext context) {
|
||||||
this(locationOfUseAddress, null, null, LocationReferenceContext.get(context), false);
|
this(locationOfUseAddress, null, null, SearchLocationContext.get(context), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
LocationReference(Address locationOfUseAddress, String context, ProgramLocation location) {
|
LocationReference(Address locationOfUseAddress, String context, ProgramLocation location) {
|
||||||
this(locationOfUseAddress, location, null, LocationReferenceContext.get(context), false);
|
this(locationOfUseAddress, location, null, SearchLocationContext.get(context), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -128,7 +129,7 @@ public class LocationReference implements Comparable<LocationReference> {
|
|||||||
*
|
*
|
||||||
* @return the context
|
* @return the context
|
||||||
*/
|
*/
|
||||||
public LocationReferenceContext getContext() {
|
public SearchLocationContext getContext() {
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import java.util.function.Supplier;
|
|||||||
import docking.ActionContext;
|
import docking.ActionContext;
|
||||||
import docking.action.DockingAction;
|
import docking.action.DockingAction;
|
||||||
import docking.action.builder.ActionBuilder;
|
import docking.action.builder.ActionBuilder;
|
||||||
|
import docking.widgets.table.actions.DeleteTableRowAction;
|
||||||
import ghidra.app.CorePluginPackage;
|
import ghidra.app.CorePluginPackage;
|
||||||
import ghidra.app.context.ListingActionContext;
|
import ghidra.app.context.ListingActionContext;
|
||||||
import ghidra.app.events.ProgramClosedPluginEvent;
|
import ghidra.app.events.ProgramClosedPluginEvent;
|
||||||
@@ -39,7 +40,6 @@ import ghidra.program.model.symbol.Reference;
|
|||||||
import ghidra.program.util.ProgramLocation;
|
import ghidra.program.util.ProgramLocation;
|
||||||
import ghidra.util.HelpLocation;
|
import ghidra.util.HelpLocation;
|
||||||
import ghidra.util.Msg;
|
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.
|
* Plugin to show a list of references to the item represented by the location of the cursor.
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import docking.ActionContext;
|
|||||||
import docking.action.*;
|
import docking.action.*;
|
||||||
import docking.action.builder.ActionBuilder;
|
import docking.action.builder.ActionBuilder;
|
||||||
import docking.widgets.table.GTable;
|
import docking.widgets.table.GTable;
|
||||||
|
import docking.widgets.table.actions.DeleteTableRowAction;
|
||||||
import generic.theme.GIcon;
|
import generic.theme.GIcon;
|
||||||
import ghidra.app.cmd.refs.RemoveReferenceCmd;
|
import ghidra.app.cmd.refs.RemoveReferenceCmd;
|
||||||
import ghidra.app.nav.Navigatable;
|
import ghidra.app.nav.Navigatable;
|
||||||
@@ -45,7 +46,6 @@ import ghidra.program.util.ProgramLocation;
|
|||||||
import ghidra.util.HelpLocation;
|
import ghidra.util.HelpLocation;
|
||||||
import ghidra.util.table.GhidraTable;
|
import ghidra.util.table.GhidraTable;
|
||||||
import ghidra.util.table.SelectionNavigationAction;
|
import ghidra.util.table.SelectionNavigationAction;
|
||||||
import ghidra.util.table.actions.DeleteTableRowAction;
|
|
||||||
import ghidra.util.table.actions.MakeProgramSelectionAction;
|
import ghidra.util.table.actions.MakeProgramSelectionAction;
|
||||||
import ghidra.util.task.SwingUpdateManager;
|
import ghidra.util.task.SwingUpdateManager;
|
||||||
import resources.Icons;
|
import resources.Icons;
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* 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 org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import docking.widgets.search.SearchLocationContext;
|
||||||
import docking.widgets.table.GTableCellRenderingData;
|
import docking.widgets.table.GTableCellRenderingData;
|
||||||
import ghidra.docking.settings.Settings;
|
import ghidra.docking.settings.Settings;
|
||||||
import ghidra.framework.plugintool.ServiceProvider;
|
import ghidra.framework.plugintool.ServiceProvider;
|
||||||
@@ -176,7 +177,7 @@ class LocationReferencesTableModel extends AddressBasedTableModel<LocationRefere
|
|||||||
}
|
}
|
||||||
|
|
||||||
// when the row object does not represent an applied reference, then it may have context
|
// when the row object does not represent an applied reference, then it may have context
|
||||||
LocationReferenceContext context = rowObject.getContext();
|
SearchLocationContext context = rowObject.getContext();
|
||||||
String text = context.getBoldMatchingText();
|
String text = context.getBoldMatchingText();
|
||||||
setText(text);
|
setText(text);
|
||||||
return this;
|
return this;
|
||||||
@@ -202,7 +203,7 @@ class LocationReferencesTableModel extends AddressBasedTableModel<LocationRefere
|
|||||||
return refTypeString;
|
return refTypeString;
|
||||||
}
|
}
|
||||||
|
|
||||||
LocationReferenceContext context = rowObject.getContext();
|
SearchLocationContext context = rowObject.getContext();
|
||||||
return context.getPlainText();
|
return context.getPlainText();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import java.util.Stack;
|
|||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import docking.widgets.search.SearchLocationContext;
|
||||||
import ghidra.app.services.*;
|
import ghidra.app.services.*;
|
||||||
import ghidra.program.model.address.*;
|
import ghidra.program.model.address.*;
|
||||||
import ghidra.program.model.data.*;
|
import ghidra.program.model.data.*;
|
||||||
@@ -343,7 +344,7 @@ public final class ReferenceUtils {
|
|||||||
|
|
||||||
Consumer<DataTypeReference> callback = ref -> {
|
Consumer<DataTypeReference> callback = ref -> {
|
||||||
|
|
||||||
LocationReferenceContext context = ref.getContext();
|
SearchLocationContext context = ref.getContext();
|
||||||
LocationReference locationReference = new LocationReference(ref.getAddress(), context);
|
LocationReference locationReference = new LocationReference(ref.getAddress(), context);
|
||||||
accumulator.add(locationReference);
|
accumulator.add(locationReference);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* 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.DockingAction;
|
||||||
import docking.action.MenuData;
|
import docking.action.MenuData;
|
||||||
import docking.tool.ToolConstants;
|
import docking.tool.ToolConstants;
|
||||||
|
import docking.widgets.table.actions.DeleteTableRowAction;
|
||||||
import ghidra.app.CorePluginPackage;
|
import ghidra.app.CorePluginPackage;
|
||||||
import ghidra.app.context.NavigatableActionContext;
|
import ghidra.app.context.NavigatableActionContext;
|
||||||
import ghidra.app.context.NavigatableContextAction;
|
import ghidra.app.context.NavigatableContextAction;
|
||||||
@@ -37,7 +38,6 @@ import ghidra.framework.plugintool.PluginTool;
|
|||||||
import ghidra.framework.plugintool.util.PluginStatus;
|
import ghidra.framework.plugintool.util.PluginStatus;
|
||||||
import ghidra.program.model.listing.Program;
|
import ghidra.program.model.listing.Program;
|
||||||
import ghidra.util.HelpLocation;
|
import ghidra.util.HelpLocation;
|
||||||
import ghidra.util.table.actions.DeleteTableRowAction;
|
|
||||||
import ghidra.util.task.SwingUpdateManager;
|
import ghidra.util.task.SwingUpdateManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import docking.*;
|
|||||||
import docking.widgets.label.GLabel;
|
import docking.widgets.label.GLabel;
|
||||||
import docking.widgets.table.GTableFilterPanel;
|
import docking.widgets.table.GTableFilterPanel;
|
||||||
import docking.widgets.table.TableFilter;
|
import docking.widgets.table.TableFilter;
|
||||||
|
import docking.widgets.table.actions.DeleteTableRowAction;
|
||||||
import generic.theme.GIcon;
|
import generic.theme.GIcon;
|
||||||
import ghidra.app.plugin.core.scalartable.RangeFilterTextField.FilterType;
|
import ghidra.app.plugin.core.scalartable.RangeFilterTextField.FilterType;
|
||||||
import ghidra.framework.plugintool.ComponentProviderAdapter;
|
import ghidra.framework.plugintool.ComponentProviderAdapter;
|
||||||
@@ -35,7 +36,6 @@ import ghidra.program.model.scalar.Scalar;
|
|||||||
import ghidra.program.util.ProgramSelection;
|
import ghidra.program.util.ProgramSelection;
|
||||||
import ghidra.util.HelpLocation;
|
import ghidra.util.HelpLocation;
|
||||||
import ghidra.util.table.*;
|
import ghidra.util.table.*;
|
||||||
import ghidra.util.table.actions.DeleteTableRowAction;
|
|
||||||
import ghidra.util.table.actions.MakeProgramSelectionAction;
|
import ghidra.util.table.actions.MakeProgramSelectionAction;
|
||||||
import help.HelpService;
|
import help.HelpService;
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import docking.action.DockingAction;
|
|||||||
import docking.action.MenuData;
|
import docking.action.MenuData;
|
||||||
import docking.widgets.table.AbstractSortedTableModel;
|
import docking.widgets.table.AbstractSortedTableModel;
|
||||||
import docking.widgets.table.GTable;
|
import docking.widgets.table.GTable;
|
||||||
|
import docking.widgets.table.actions.DeleteTableRowAction;
|
||||||
import generic.theme.GIcon;
|
import generic.theme.GIcon;
|
||||||
import ghidra.app.nav.Navigatable;
|
import ghidra.app.nav.Navigatable;
|
||||||
import ghidra.app.nav.NavigatableRemovalListener;
|
import ghidra.app.nav.NavigatableRemovalListener;
|
||||||
@@ -44,7 +45,6 @@ import ghidra.program.model.listing.Program;
|
|||||||
import ghidra.program.util.*;
|
import ghidra.program.util.*;
|
||||||
import ghidra.util.HelpLocation;
|
import ghidra.util.HelpLocation;
|
||||||
import ghidra.util.table.*;
|
import ghidra.util.table.*;
|
||||||
import ghidra.util.table.actions.DeleteTableRowAction;
|
|
||||||
import ghidra.util.table.actions.MakeProgramSelectionAction;
|
import ghidra.util.table.actions.MakeProgramSelectionAction;
|
||||||
import utility.function.Callback;
|
import utility.function.Callback;
|
||||||
import utility.function.Dummy;
|
import utility.function.Dummy;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import java.util.*;
|
|||||||
|
|
||||||
import javax.swing.Icon;
|
import javax.swing.Icon;
|
||||||
|
|
||||||
|
import docking.widgets.table.actions.DeleteTableRowAction;
|
||||||
import ghidra.app.CorePluginPackage;
|
import ghidra.app.CorePluginPackage;
|
||||||
import ghidra.app.events.ProgramClosedPluginEvent;
|
import ghidra.app.events.ProgramClosedPluginEvent;
|
||||||
import ghidra.app.nav.Navigatable;
|
import ghidra.app.nav.Navigatable;
|
||||||
@@ -38,7 +39,6 @@ import ghidra.framework.plugintool.util.PluginStatus;
|
|||||||
import ghidra.program.model.listing.Program;
|
import ghidra.program.model.listing.Program;
|
||||||
import ghidra.util.Swing;
|
import ghidra.util.Swing;
|
||||||
import ghidra.util.table.GhidraProgramTableModel;
|
import ghidra.util.table.GhidraProgramTableModel;
|
||||||
import ghidra.util.table.actions.DeleteTableRowAction;
|
|
||||||
import ghidra.util.task.SwingUpdateManager;
|
import ghidra.util.task.SwingUpdateManager;
|
||||||
|
|
||||||
//@formatter:off
|
//@formatter:off
|
||||||
@@ -170,9 +170,7 @@ public class TableServicePlugin extends ProgramPlugin
|
|||||||
}
|
}
|
||||||
|
|
||||||
void remove(TableComponentProvider<?> provider) {
|
void remove(TableComponentProvider<?> provider) {
|
||||||
Iterator<Program> iter = programMap.keySet().iterator();
|
for (Program p : programMap.keySet()) {
|
||||||
while (iter.hasNext()) {
|
|
||||||
Program p = iter.next();
|
|
||||||
List<TableComponentProvider<?>> list = programMap.get(p);
|
List<TableComponentProvider<?>> list = programMap.get(p);
|
||||||
if (list.remove(provider)) {
|
if (list.remove(provider)) {
|
||||||
if (list.size() == 0) {
|
if (list.size() == 0) {
|
||||||
@@ -184,9 +182,7 @@ public class TableServicePlugin extends ProgramPlugin
|
|||||||
}
|
}
|
||||||
|
|
||||||
void removeDialog(TableServiceTableChooserDialog dialog) {
|
void removeDialog(TableServiceTableChooserDialog dialog) {
|
||||||
Iterator<Program> iter = programToDialogMap.keySet().iterator();
|
for (Program p : programToDialogMap.keySet()) {
|
||||||
while (iter.hasNext()) {
|
|
||||||
Program p = iter.next();
|
|
||||||
List<TableChooserDialog> list = programToDialogMap.get(p);
|
List<TableChooserDialog> list = programToDialogMap.get(p);
|
||||||
if (list.remove(dialog)) {
|
if (list.remove(dialog)) {
|
||||||
if (list.size() == 0) {
|
if (list.size() == 0) {
|
||||||
@@ -213,9 +209,7 @@ public class TableServicePlugin extends ProgramPlugin
|
|||||||
|
|
||||||
private List<TableComponentProvider<?>> getProviders() {
|
private List<TableComponentProvider<?>> getProviders() {
|
||||||
List<TableComponentProvider<?>> clist = new ArrayList<>();
|
List<TableComponentProvider<?>> clist = new ArrayList<>();
|
||||||
Iterator<List<TableComponentProvider<?>>> iter = programMap.values().iterator();
|
for (List<TableComponentProvider<?>> list : programMap.values()) {
|
||||||
while (iter.hasNext()) {
|
|
||||||
List<TableComponentProvider<?>> list = iter.next();
|
|
||||||
clist.addAll(list);
|
clist.addAll(list);
|
||||||
}
|
}
|
||||||
return clist;
|
return clist;
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package ghidra.app.services;
|
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.address.Address;
|
||||||
import ghidra.program.model.data.DataType;
|
import ghidra.program.model.data.DataType;
|
||||||
import ghidra.program.model.listing.Function;
|
import ghidra.program.model.listing.Function;
|
||||||
@@ -31,10 +31,10 @@ public class DataTypeReference {
|
|||||||
private Address address;
|
private Address address;
|
||||||
|
|
||||||
/** A preview of how the reference was used */
|
/** A preview of how the reference was used */
|
||||||
private LocationReferenceContext context;
|
private SearchLocationContext context;
|
||||||
|
|
||||||
public DataTypeReference(DataType dataType, String fieldName, Function function,
|
public DataTypeReference(DataType dataType, String fieldName, Function function,
|
||||||
Address address, LocationReferenceContext context) {
|
Address address, SearchLocationContext context) {
|
||||||
this.dataType = dataType;
|
this.dataType = dataType;
|
||||||
this.fieldName = fieldName;
|
this.fieldName = fieldName;
|
||||||
this.function = function;
|
this.function = function;
|
||||||
@@ -54,7 +54,7 @@ public class DataTypeReference {
|
|||||||
return address;
|
return address;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LocationReferenceContext getContext() {
|
public SearchLocationContext getContext() {
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import docking.action.builder.ToggleActionBuilder;
|
|||||||
import docking.util.GGlassPaneMessage;
|
import docking.util.GGlassPaneMessage;
|
||||||
import docking.widgets.OptionDialog;
|
import docking.widgets.OptionDialog;
|
||||||
import docking.widgets.OptionDialogBuilder;
|
import docking.widgets.OptionDialogBuilder;
|
||||||
|
import docking.widgets.table.actions.DeleteTableRowAction;
|
||||||
import generic.theme.GIcon;
|
import generic.theme.GIcon;
|
||||||
import ghidra.app.context.NavigatableActionContext;
|
import ghidra.app.context.NavigatableActionContext;
|
||||||
import ghidra.app.nav.Navigatable;
|
import ghidra.app.nav.Navigatable;
|
||||||
@@ -57,7 +58,6 @@ import ghidra.util.Msg;
|
|||||||
import ghidra.util.layout.VerticalLayout;
|
import ghidra.util.layout.VerticalLayout;
|
||||||
import ghidra.util.table.GhidraTable;
|
import ghidra.util.table.GhidraTable;
|
||||||
import ghidra.util.table.SelectionNavigationAction;
|
import ghidra.util.table.SelectionNavigationAction;
|
||||||
import ghidra.util.table.actions.DeleteTableRowAction;
|
|
||||||
import ghidra.util.table.actions.MakeProgramSelectionAction;
|
import ghidra.util.table.actions.MakeProgramSelectionAction;
|
||||||
import resources.Icons;
|
import resources.Icons;
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import docking.action.ToggleDockingAction;
|
|||||||
import docking.action.builder.ActionBuilder;
|
import docking.action.builder.ActionBuilder;
|
||||||
import docking.action.builder.ToggleActionBuilder;
|
import docking.action.builder.ToggleActionBuilder;
|
||||||
import docking.widgets.table.GTable;
|
import docking.widgets.table.GTable;
|
||||||
|
import docking.widgets.table.actions.DeleteTableRowAction;
|
||||||
import docking.widgets.table.threaded.ThreadedTableModel;
|
import docking.widgets.table.threaded.ThreadedTableModel;
|
||||||
import generic.theme.GIcon;
|
import generic.theme.GIcon;
|
||||||
import ghidra.app.nav.Navigatable;
|
import ghidra.app.nav.Navigatable;
|
||||||
@@ -38,7 +39,6 @@ import ghidra.program.model.listing.Program;
|
|||||||
import ghidra.program.util.ProgramTask;
|
import ghidra.program.util.ProgramTask;
|
||||||
import ghidra.util.HelpLocation;
|
import ghidra.util.HelpLocation;
|
||||||
import ghidra.util.table.*;
|
import ghidra.util.table.*;
|
||||||
import ghidra.util.table.actions.DeleteTableRowAction;
|
|
||||||
import ghidra.util.table.actions.MakeProgramSelectionAction;
|
import ghidra.util.table.actions.MakeProgramSelectionAction;
|
||||||
import ghidra.util.task.TaskLauncher;
|
import ghidra.util.task.TaskLauncher;
|
||||||
import ghidra.util.task.TaskMonitor;
|
import ghidra.util.task.TaskMonitor;
|
||||||
|
|||||||
@@ -281,6 +281,8 @@ public class GhidraTable extends GTable {
|
|||||||
* <p>
|
* <p>
|
||||||
* This method differs from {@link #navigate(int, int)} in that this method will not navigate if
|
* This method differs from {@link #navigate(int, int)} in that this method will not navigate if
|
||||||
* {@link #navigateOnSelection} is <code>false</code>.
|
* {@link #navigateOnSelection} is <code>false</code>.
|
||||||
|
* @param row the row
|
||||||
|
* @param column the column
|
||||||
*/
|
*/
|
||||||
protected void navigateOnCurrentSelection(int row, int column) {
|
protected void navigateOnCurrentSelection(int row, int column) {
|
||||||
if (!navigateOnSelection) {
|
if (!navigateOnSelection) {
|
||||||
|
|||||||
@@ -31,8 +31,9 @@ import org.junit.*;
|
|||||||
|
|
||||||
import docking.action.DockingActionIf;
|
import docking.action.DockingActionIf;
|
||||||
import docking.util.AnimationUtils;
|
import docking.util.AnimationUtils;
|
||||||
import docking.widgets.FindDialog;
|
import docking.widgets.*;
|
||||||
import docking.widgets.TextComponentSearcher;
|
import docking.widgets.search.TextComponentSearcher;
|
||||||
|
import docking.widgets.table.GTable;
|
||||||
import generic.jar.ResourceFile;
|
import generic.jar.ResourceFile;
|
||||||
import generic.theme.GColor;
|
import generic.theme.GColor;
|
||||||
import ghidra.app.script.GhidraScript;
|
import ghidra.app.script.GhidraScript;
|
||||||
@@ -78,8 +79,6 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
|
|||||||
|
|
||||||
placeCursorAtBeginning();
|
placeCursorAtBeginning();
|
||||||
findDialog = showFindDialog();
|
findDialog = showFindDialog();
|
||||||
String searchText = "Hello";
|
|
||||||
find(searchText);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@@ -88,12 +87,47 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
|
|||||||
env.dispose();
|
env.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFindAll() throws Exception {
|
||||||
|
|
||||||
|
String searchText = "Hello";
|
||||||
|
findAll(searchText);
|
||||||
|
assertFalse(findDialog.isShowing());
|
||||||
|
List<TestTextMatch> matches = getExpectedMatches(searchText);
|
||||||
|
assertEquals(3, matches.size());
|
||||||
|
|
||||||
|
verfiyHighlightColor(matches);
|
||||||
|
|
||||||
|
FindDialogResultsProvider resultsProvider =
|
||||||
|
waitForComponentProvider(FindDialogResultsProvider.class);
|
||||||
|
List<SearchLocation> results = resultsProvider.getResults();
|
||||||
|
assertEquals(matches.size(), results.size());
|
||||||
|
|
||||||
|
int row = 0;
|
||||||
|
selectRow(resultsProvider, row);
|
||||||
|
assertActiveHighlight(matches.get(row));
|
||||||
|
assertCursorInMatch(matches.get(row));
|
||||||
|
|
||||||
|
row = 1;
|
||||||
|
selectRow(resultsProvider, row);
|
||||||
|
assertActiveHighlight(matches.get(row));
|
||||||
|
assertCursorInMatch(matches.get(row));
|
||||||
|
|
||||||
|
row = 2;
|
||||||
|
selectRow(resultsProvider, row);
|
||||||
|
assertActiveHighlight(matches.get(row));
|
||||||
|
assertCursorInMatch(matches.get(row));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testFindHighlights() throws Exception {
|
public void testFindHighlights() throws Exception {
|
||||||
|
|
||||||
List<TestTextMatch> matches = getMatches();
|
String searchText = "Hello";
|
||||||
|
find(searchText);
|
||||||
|
|
||||||
|
List<TestTextMatch> matches = getExpectedMatches();
|
||||||
assertEquals(3, matches.size());
|
assertEquals(3, matches.size());
|
||||||
verfyHighlightColor(matches);
|
verfiyHighlightColor(matches);
|
||||||
|
|
||||||
close(findDialog);
|
close(findDialog);
|
||||||
verifyDefaultBackgroundColorForAllText();
|
verifyDefaultBackgroundColorForAllText();
|
||||||
@@ -102,18 +136,21 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testFindHighlights_ChangeSearchText() throws Exception {
|
public void testFindHighlights_ChangeSearchText() throws Exception {
|
||||||
|
|
||||||
List<TestTextMatch> matches = getMatches();
|
String searchText = "Hello";
|
||||||
|
find(searchText);
|
||||||
|
|
||||||
|
List<TestTextMatch> matches = getExpectedMatches();
|
||||||
assertEquals(3, matches.size());
|
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
|
// Change the search text after the first search and make sure the new text is found and
|
||||||
// highlighted correctly.
|
// highlighted correctly.
|
||||||
String newSearchText = "java";
|
String newSearchText = "java";
|
||||||
runSwing(() -> findDialog.setSearchText(newSearchText));
|
runSwing(() -> findDialog.setSearchText(newSearchText));
|
||||||
pressButtonByText(findDialog, "Next");
|
pressButtonByText(findDialog, "Next");
|
||||||
matches = getMatches();
|
matches = getExpectedMatches();
|
||||||
assertEquals(2, matches.size());
|
assertEquals(2, matches.size());
|
||||||
verfyHighlightColor(matches);
|
verfiyHighlightColor(matches);
|
||||||
|
|
||||||
close(findDialog);
|
close(findDialog);
|
||||||
verifyDefaultBackgroundColorForAllText();
|
verifyDefaultBackgroundColorForAllText();
|
||||||
@@ -122,9 +159,12 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testFindHighlights_ChangeDocumentText() throws Exception {
|
public void testFindHighlights_ChangeDocumentText() throws Exception {
|
||||||
|
|
||||||
List<TestTextMatch> matches = getMatches();
|
String searchText = "Hello";
|
||||||
|
find(searchText);
|
||||||
|
|
||||||
|
List<TestTextMatch> matches = getExpectedMatches();
|
||||||
assertEquals(3, matches.size());
|
assertEquals(3, matches.size());
|
||||||
verfyHighlightColor(matches);
|
verfiyHighlightColor(matches);
|
||||||
|
|
||||||
runSwing(() -> textPane.setText("This is some\nnew text."));
|
runSwing(() -> textPane.setText("This is some\nnew text."));
|
||||||
|
|
||||||
@@ -135,7 +175,10 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testMovingCursorUpdatesActiveHighlight() {
|
public void testMovingCursorUpdatesActiveHighlight() {
|
||||||
|
|
||||||
List<TestTextMatch> matches = getMatches();
|
String searchText = "Hello";
|
||||||
|
find(searchText);
|
||||||
|
|
||||||
|
List<TestTextMatch> matches = getExpectedMatches();
|
||||||
assertEquals(3, matches.size());
|
assertEquals(3, matches.size());
|
||||||
TestTextMatch first = matches.get(0);
|
TestTextMatch first = matches.get(0);
|
||||||
TestTextMatch second = matches.get(1);
|
TestTextMatch second = matches.get(1);
|
||||||
@@ -154,7 +197,10 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testFindNext_ChangeDocumentText() throws Exception {
|
public void testFindNext_ChangeDocumentText() throws Exception {
|
||||||
|
|
||||||
List<TestTextMatch> matches = getMatches();
|
String searchText = "Hello";
|
||||||
|
find(searchText);
|
||||||
|
|
||||||
|
List<TestTextMatch> matches = getExpectedMatches();
|
||||||
assertEquals(3, matches.size());
|
assertEquals(3, matches.size());
|
||||||
TestTextMatch first = matches.get(0);
|
TestTextMatch first = matches.get(0);
|
||||||
TestTextMatch second = matches.get(1);
|
TestTextMatch second = matches.get(1);
|
||||||
@@ -177,11 +223,11 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
|
|||||||
// worth worrying about.)
|
// worth worrying about.)
|
||||||
next();
|
next();
|
||||||
|
|
||||||
matches = getMatches();
|
matches = getExpectedMatches();
|
||||||
assertEquals(5, matches.size()); // 3 old matches plus 2 new matches
|
assertEquals(5, matches.size()); // 3 old matches plus 2 new matches
|
||||||
second = matches.get(1);
|
TestTextMatch third = matches.get(1);
|
||||||
assertCursorInMatch(second);
|
assertCursorInMatch(third);
|
||||||
assertActiveHighlight(second);
|
assertActiveHighlight(third);
|
||||||
|
|
||||||
next(); // third
|
next(); // third
|
||||||
next(); // fourth
|
next(); // fourth
|
||||||
@@ -193,10 +239,28 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
|
|||||||
close(findDialog);
|
close(findDialog);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFindNext_NoMatches() throws Exception {
|
||||||
|
|
||||||
|
String searchText = "Goodbye";
|
||||||
|
find(searchText);
|
||||||
|
|
||||||
|
List<TestTextMatch> matches = getExpectedMatches();
|
||||||
|
assertEquals(0, matches.size());
|
||||||
|
|
||||||
|
String status = runSwing(() -> findDialog.getStatusText());
|
||||||
|
assertEquals("Not found", status);
|
||||||
|
|
||||||
|
runSwing(() -> findDialog.dispose());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testFindNext() throws Exception {
|
public void testFindNext() throws Exception {
|
||||||
|
|
||||||
List<TestTextMatch> matches = getMatches();
|
String searchText = "Hello";
|
||||||
|
find(searchText);
|
||||||
|
|
||||||
|
List<TestTextMatch> matches = getExpectedMatches();
|
||||||
assertEquals(3, matches.size());
|
assertEquals(3, matches.size());
|
||||||
TestTextMatch first = matches.get(0);
|
TestTextMatch first = matches.get(0);
|
||||||
TestTextMatch second = matches.get(1);
|
TestTextMatch second = matches.get(1);
|
||||||
@@ -224,7 +288,10 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testFindNext_MoveCaret() throws Exception {
|
public void testFindNext_MoveCaret() throws Exception {
|
||||||
|
|
||||||
List<TestTextMatch> matches = getMatches();
|
String searchText = "Hello";
|
||||||
|
find(searchText);
|
||||||
|
|
||||||
|
List<TestTextMatch> matches = getExpectedMatches();
|
||||||
assertEquals(3, matches.size());
|
assertEquals(3, matches.size());
|
||||||
TestTextMatch first = matches.get(0);
|
TestTextMatch first = matches.get(0);
|
||||||
TestTextMatch second = matches.get(1);
|
TestTextMatch second = matches.get(1);
|
||||||
@@ -247,7 +314,10 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testFindPrevious() throws Exception {
|
public void testFindPrevious() throws Exception {
|
||||||
|
|
||||||
List<TestTextMatch> matches = getMatches();
|
String searchText = "Hello";
|
||||||
|
find(searchText);
|
||||||
|
|
||||||
|
List<TestTextMatch> matches = getExpectedMatches();
|
||||||
assertEquals(3, matches.size());
|
assertEquals(3, matches.size());
|
||||||
TestTextMatch first = matches.get(0);
|
TestTextMatch first = matches.get(0);
|
||||||
TestTextMatch second = matches.get(1);
|
TestTextMatch second = matches.get(1);
|
||||||
@@ -277,7 +347,10 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testFindPrevious_MoveCaret() throws Exception {
|
public void testFindPrevious_MoveCaret() throws Exception {
|
||||||
|
|
||||||
List<TestTextMatch> matches = getMatches();
|
String searchText = "Hello";
|
||||||
|
find(searchText);
|
||||||
|
|
||||||
|
List<TestTextMatch> matches = getExpectedMatches();
|
||||||
assertEquals(3, matches.size());
|
assertEquals(3, matches.size());
|
||||||
TestTextMatch first = matches.get(0);
|
TestTextMatch first = matches.get(0);
|
||||||
TestTextMatch second = matches.get(1);
|
TestTextMatch second = matches.get(1);
|
||||||
@@ -300,9 +373,12 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testClear() throws Exception {
|
public void testClear() throws Exception {
|
||||||
|
|
||||||
List<TestTextMatch> matches = getMatches();
|
String searchText = "Hello";
|
||||||
|
find(searchText);
|
||||||
|
|
||||||
|
List<TestTextMatch> matches = getExpectedMatches();
|
||||||
assertEquals(3, matches.size());
|
assertEquals(3, matches.size());
|
||||||
verfyHighlightColor(matches);
|
verfiyHighlightColor(matches);
|
||||||
|
|
||||||
clear();
|
clear();
|
||||||
|
|
||||||
@@ -331,17 +407,26 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
|
|||||||
|
|
||||||
private void next() {
|
private void next() {
|
||||||
pressButtonByText(findDialog, "Next");
|
pressButtonByText(findDialog, "Next");
|
||||||
waitForSwing();
|
waitForTasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void previous() {
|
private void previous() {
|
||||||
pressButtonByText(findDialog, "Previous");
|
pressButtonByText(findDialog, "Previous");
|
||||||
waitForSwing();
|
waitForTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void findAll(String text) {
|
||||||
|
runSwing(() -> findDialog.setSearchText(text));
|
||||||
|
pressButtonByText(findDialog, "Find All");
|
||||||
|
waitForTasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertSearchModelHasNoSearchResults() {
|
private void assertSearchModelHasNoSearchResults() {
|
||||||
TextComponentSearcher searcher =
|
TextComponentSearcher searcher =
|
||||||
(TextComponentSearcher) findDialog.getSearcher();
|
(TextComponentSearcher) findDialog.getSearcher();
|
||||||
|
if (searcher == null) {
|
||||||
|
return; // assume the searcher was disposed
|
||||||
|
}
|
||||||
assertFalse(searcher.hasSearchResults());
|
assertFalse(searcher.hasSearchResults());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,10 +446,11 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
|
|||||||
private void assertActiveHighlight(TestTextMatch match) {
|
private void assertActiveHighlight(TestTextMatch match) {
|
||||||
|
|
||||||
GColor expectedHlColor = new GColor("color.bg.find.highlight.active");
|
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(() -> {
|
Highlight matchHighlight = runSwing(() -> {
|
||||||
|
|
||||||
Highlighter highlighter = textPane.getHighlighter();
|
Highlighter highlighter = textPane.getHighlighter();
|
||||||
@@ -379,10 +465,14 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
assertNotNull(matchHighlight);
|
assertNotNull("No highlight found for " + match, matchHighlight);
|
||||||
DefaultHighlightPainter painter = (DefaultHighlightPainter) matchHighlight.getPainter();
|
DefaultHighlightPainter painter = (DefaultHighlightPainter) matchHighlight.getPainter();
|
||||||
Color actualHlColor = painter.getColor();
|
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() {
|
private void placeCursorAtBeginning() {
|
||||||
@@ -396,7 +486,12 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
|
|||||||
waitForSwing();
|
waitForSwing();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void verfyHighlightColor(List<TestTextMatch> matches)
|
private void selectRow(FindDialogResultsProvider resultsProvider, int row) {
|
||||||
|
GTable table = resultsProvider.getTable();
|
||||||
|
runSwing(() -> table.selectRow(row));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verfiyHighlightColor(List<TestTextMatch> matches)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
|
|
||||||
GColor nonActiveHlColor = new GColor("color.bg.find.highlight");
|
GColor nonActiveHlColor = new GColor("color.bg.find.highlight");
|
||||||
@@ -405,10 +500,11 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
|
|||||||
int caret = textPane.getCaretPosition();
|
int caret = textPane.getCaretPosition();
|
||||||
for (TestTextMatch match : matches) {
|
for (TestTextMatch match : matches) {
|
||||||
Color expectedColor = nonActiveHlColor;
|
Color expectedColor = nonActiveHlColor;
|
||||||
if (match.contains(caret)) {
|
boolean isActive = match.contains(caret);
|
||||||
|
if (isActive) {
|
||||||
expectedColor = activeHlColor;
|
expectedColor = activeHlColor;
|
||||||
}
|
}
|
||||||
assertActiveHighlight(match, expectedColor);
|
assertActiveHighlight(match, expectedColor, isActive);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,9 +522,14 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<TestTextMatch> getMatches() {
|
private List<TestTextMatch> getExpectedMatches() {
|
||||||
|
|
||||||
String searchText = findDialog.getSearchText();
|
String searchText = findDialog.getSearchText();
|
||||||
|
assertFalse(searchText.isEmpty());
|
||||||
|
return getExpectedMatches(searchText);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TestTextMatch> getExpectedMatches(String searchText) {
|
||||||
List<TestTextMatch> results = new ArrayList<>();
|
List<TestTextMatch> results = new ArrayList<>();
|
||||||
String text = runSwing(() -> textPane.getText());
|
String text = runSwing(() -> textPane.getText());
|
||||||
|
|
||||||
@@ -449,8 +550,7 @@ public class ConsolePluginTest extends AbstractGhidraHeadedIntegrationTest {
|
|||||||
|
|
||||||
private void find(String text) {
|
private void find(String text) {
|
||||||
runSwing(() -> findDialog.setSearchText(text));
|
runSwing(() -> findDialog.setSearchText(text));
|
||||||
pressButtonByText(findDialog, "Next");
|
next();
|
||||||
waitForTasks();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private FindDialog showFindDialog() {
|
private FindDialog showFindDialog() {
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ public class DecompilerDiffViewFindAction extends DockingAction {
|
|||||||
|
|
||||||
private FindDialog createFindDialog(DecompilerPanel decompilerPanel, Side side) {
|
private FindDialog createFindDialog(DecompilerPanel decompilerPanel, Side side) {
|
||||||
String title = (side == LEFT ? "Left" : "Right");
|
String title = (side == LEFT ? "Left" : "Right");
|
||||||
title += " Decompiler Find Text";
|
title += " Decompiler Find";
|
||||||
|
|
||||||
FindDialog dialog = new FindDialog(title, new DecompilerSearcher(decompilerPanel)) {
|
FindDialog dialog = new FindDialog(title, new DecompilerSearcher(decompilerPanel)) {
|
||||||
@Override
|
@Override
|
||||||
@@ -98,6 +98,15 @@ public class DecompilerDiffViewFindAction extends DockingAction {
|
|||||||
};
|
};
|
||||||
dialog.setHelpLocation(new HelpLocation(HelpTopics.DECOMPILER, "ActionFind"));
|
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;
|
return dialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ color.bg.decompiler.current.variable = color.palette.highlight.transparent.yello
|
|||||||
color.bg.decompiler.highlights.middle.mouse = color.bg.highlight
|
color.bg.decompiler.highlights.middle.mouse = color.bg.highlight
|
||||||
color.bg.decompiler.highlights.default = color.palette.highlight.transparent.yellow
|
color.bg.decompiler.highlights.default = color.palette.highlight.transparent.yellow
|
||||||
color.bg.decompiler.highlights.special = color.palette.crimson
|
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.default = color.palette.red
|
||||||
color.bg.decompiler.pcode.dfg.vertex.selected = color.palette.lightcoral
|
color.bg.decompiler.pcode.dfg.vertex.selected = color.palette.lightcoral
|
||||||
|
|||||||
@@ -443,6 +443,7 @@ public class DecompileOptions {
|
|||||||
|
|
||||||
private static final String SEARCH_HIGHLIGHT_MSG = "Display.Color for Highlighting Find Matches";
|
private static final 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_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 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");
|
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 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
|
* @return color used to highlight search results
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -15,251 +15,16 @@
|
|||||||
*/
|
*/
|
||||||
package ghidra.app.decompiler.component;
|
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.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.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.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.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 {
|
public class DecompilerFindDialog extends FindDialog {
|
||||||
|
|
||||||
private DecompilerPanel decompilerPanel;
|
|
||||||
private GButton showAllButton;
|
|
||||||
|
|
||||||
public DecompilerFindDialog(DecompilerPanel decompilerPanel) {
|
public DecompilerFindDialog(DecompilerPanel decompilerPanel) {
|
||||||
super("Decompiler Find Text", new DecompilerSearcher(decompilerPanel));
|
super("Decompiler Find", new DecompilerSearcher(decompilerPanel));
|
||||||
this.decompilerPanel = decompilerPanel;
|
|
||||||
|
|
||||||
setHelpLocation(new HelpLocation(HelpTopics.DECOMPILER, "ActionFind"));
|
setHelpLocation(new HelpLocation(HelpTopics.DECOMPILER, "ActionFind"));
|
||||||
|
|
||||||
showAllButton = new GButton("Search All");
|
|
||||||
showAllButton.addActionListener(e -> showAll());
|
|
||||||
|
|
||||||
// move this button to the end
|
|
||||||
removeButton(dismissButton);
|
|
||||||
|
|
||||||
addButton(showAllButton);
|
|
||||||
addButton(dismissButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void enableButtons(boolean b) {
|
|
||||||
super.enableButtons(b);
|
|
||||||
showAllButton.setEnabled(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showAll() {
|
|
||||||
|
|
||||||
String searchText = getSearchText();
|
|
||||||
|
|
||||||
close();
|
|
||||||
|
|
||||||
DockingWindowManager dwm = DockingWindowManager.getActiveInstance();
|
|
||||||
Tool tool = dwm.getTool();
|
|
||||||
TableService tableService = tool.getService(TableService.class);
|
|
||||||
if (tableService == null) {
|
|
||||||
Msg.error(this,
|
|
||||||
"Cannot use the Decompiler Search All action without having a TableService " +
|
|
||||||
"installed");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<SearchLocation> results = searcher.searchAll(searchText, useRegex());
|
|
||||||
if (!results.isEmpty()) {
|
|
||||||
// save off searches that find results so users can reuse them later
|
|
||||||
storeSearchText(getSearchText());
|
|
||||||
}
|
|
||||||
|
|
||||||
Program program = decompilerPanel.getProgram();
|
|
||||||
DecompilerFindResultsModel model = new DecompilerFindResultsModel(tool, program, results);
|
|
||||||
|
|
||||||
String title = "Decompiler Search '%s'".formatted(getSearchText());
|
|
||||||
String type = "Decompiler Search Results";
|
|
||||||
String subMenuName = "Search";
|
|
||||||
TableComponentProvider<DecompilerSearchLocation> provider =
|
|
||||||
tableService.showTable(title, type, model, subMenuName, null);
|
|
||||||
|
|
||||||
// The Decompiler does not support some of the table's basic actions, such as making
|
|
||||||
// selections for a given row, so remove them.
|
|
||||||
provider.removeAllActions();
|
|
||||||
provider.installRemoveItemsAction();
|
|
||||||
|
|
||||||
GhidraThreadedTablePanel<DecompilerSearchLocation> panel = provider.getThreadedTablePanel();
|
|
||||||
GhidraTable table = panel.getTable();
|
|
||||||
|
|
||||||
// add row listener to go to the field for that row
|
|
||||||
ListSelectionModel selectionModel = table.getSelectionModel();
|
|
||||||
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
|
||||||
selectionModel.addListSelectionListener(lse -> {
|
|
||||||
if (lse.getValueIsAdjusting()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int row = table.getSelectedRow();
|
|
||||||
if (row == -1) {
|
|
||||||
searcher.highlightSearchResults(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DecompilerSearchLocation location = model.getRowObject(row);
|
|
||||||
|
|
||||||
notifySearchHit(location);
|
|
||||||
});
|
|
||||||
|
|
||||||
// add listener to table closed to clear highlights
|
|
||||||
provider.setClosedCallback(() -> decompilerPanel.setSearchResults(null));
|
|
||||||
|
|
||||||
// set the tab text to the short and descriptive search term
|
|
||||||
provider.setTabText("'%s'".formatted(getSearchText()));
|
|
||||||
}
|
|
||||||
|
|
||||||
//=================================================================================================
|
|
||||||
// Inner Classes
|
|
||||||
//=================================================================================================
|
|
||||||
|
|
||||||
private class DecompilerFindResultsModel
|
|
||||||
extends GhidraProgramTableModel<DecompilerSearchLocation> {
|
|
||||||
|
|
||||||
private List<DecompilerSearchLocation> searchLocations;
|
|
||||||
|
|
||||||
DecompilerFindResultsModel(ServiceProvider sp, Program program,
|
|
||||||
List<SearchLocation> searchLocations) {
|
|
||||||
super("Decompiler Search All Results", sp, program, null);
|
|
||||||
this.searchLocations = searchLocations.stream()
|
|
||||||
.map(l -> (DecompilerSearchLocation) l)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected TableColumnDescriptor<DecompilerSearchLocation> createTableColumnDescriptor() {
|
|
||||||
|
|
||||||
TableColumnDescriptor<DecompilerSearchLocation> descriptor =
|
|
||||||
new TableColumnDescriptor<>();
|
|
||||||
descriptor.addVisibleColumn(new LineNumberColumn(), 1, true);
|
|
||||||
descriptor.addVisibleColumn(new ContextColumn());
|
|
||||||
return descriptor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doLoad(Accumulator<DecompilerSearchLocation> accumulator,
|
|
||||||
TaskMonitor monitor)
|
|
||||||
throws CancelledException {
|
|
||||||
|
|
||||||
for (DecompilerSearchLocation location : searchLocations) {
|
|
||||||
accumulator.add(location);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ProgramLocation getProgramLocation(int modelRow, int modelColumn) {
|
|
||||||
return null; // This doesn't really make sense for this model
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ProgramSelection getProgramSelection(int[] modelRows) {
|
|
||||||
return new ProgramSelection(); // This doesn't really make sense for this model
|
|
||||||
}
|
|
||||||
|
|
||||||
private class LineNumberColumn
|
|
||||||
extends AbstractDynamicTableColumnStub<DecompilerSearchLocation, Integer> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Integer getValue(DecompilerSearchLocation rowObject, Settings settings,
|
|
||||||
ServiceProvider sp) throws IllegalArgumentException {
|
|
||||||
FieldLocation fieldLocation = rowObject.getFieldLocation();
|
|
||||||
return fieldLocation.getIndex().intValue() + 1; // +1 for 1-based lines
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getColumnName() {
|
|
||||||
return "Line";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getColumnPreferredWidth() {
|
|
||||||
return 75;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ContextColumn
|
|
||||||
extends
|
|
||||||
AbstractDynamicTableColumnStub<DecompilerSearchLocation, LocationReferenceContext> {
|
|
||||||
|
|
||||||
private GColumnRenderer<LocationReferenceContext> renderer = new ContextCellRenderer();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LocationReferenceContext getValue(DecompilerSearchLocation rowObject,
|
|
||||||
Settings settings,
|
|
||||||
ServiceProvider sp) throws IllegalArgumentException {
|
|
||||||
|
|
||||||
LocationReferenceContext context = rowObject.getContext();
|
|
||||||
return context;
|
|
||||||
// return rowObject.getTextLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getColumnName() {
|
|
||||||
return "Context";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public GColumnRenderer<LocationReferenceContext> getColumnRenderer() {
|
|
||||||
return renderer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ContextCellRenderer
|
|
||||||
extends AbstractGhidraColumnRenderer<LocationReferenceContext> {
|
|
||||||
|
|
||||||
{
|
|
||||||
// the context uses html
|
|
||||||
setHTMLRenderingEnabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Component getTableCellRendererComponent(GTableCellRenderingData data) {
|
|
||||||
|
|
||||||
// initialize
|
|
||||||
super.getTableCellRendererComponent(data);
|
|
||||||
|
|
||||||
DecompilerSearchLocation match = (DecompilerSearchLocation) data.getRowObject();
|
|
||||||
LocationReferenceContext context = match.getContext();
|
|
||||||
String text = context.getBoldMatchingText();
|
|
||||||
setText(text);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getFilterString(LocationReferenceContext context, Settings settings) {
|
|
||||||
return context.getPlainText();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import docking.DockingUtils;
|
|||||||
import docking.util.AnimationUtils;
|
import docking.util.AnimationUtils;
|
||||||
import docking.util.SwingAnimationCallback;
|
import docking.util.SwingAnimationCallback;
|
||||||
import docking.widgets.EventTrigger;
|
import docking.widgets.EventTrigger;
|
||||||
import docking.widgets.SearchLocation;
|
|
||||||
import docking.widgets.fieldpanel.FieldPanel;
|
import docking.widgets.fieldpanel.FieldPanel;
|
||||||
import docking.widgets.fieldpanel.LayoutModel;
|
import docking.widgets.fieldpanel.LayoutModel;
|
||||||
import docking.widgets.fieldpanel.field.Field;
|
import docking.widgets.fieldpanel.field.Field;
|
||||||
@@ -44,6 +43,7 @@ import ghidra.app.decompiler.component.margin.*;
|
|||||||
import ghidra.app.decompiler.location.*;
|
import ghidra.app.decompiler.location.*;
|
||||||
import ghidra.app.plugin.core.decompile.DecompilerClipboardProvider;
|
import ghidra.app.plugin.core.decompile.DecompilerClipboardProvider;
|
||||||
import ghidra.app.plugin.core.decompile.actions.DecompilerSearchLocation;
|
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.app.util.viewer.util.ScrollpaneAlignedHorizontalLayout;
|
||||||
import ghidra.program.model.address.*;
|
import ghidra.program.model.address.*;
|
||||||
import ghidra.program.model.listing.*;
|
import ghidra.program.model.listing.*;
|
||||||
@@ -95,8 +95,11 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field
|
|||||||
private int middleMouseHighlightButton;
|
private int middleMouseHighlightButton;
|
||||||
private Color middleMouseHighlightColor;
|
private Color middleMouseHighlightColor;
|
||||||
private Color currentVariableHighlightColor;
|
private Color currentVariableHighlightColor;
|
||||||
|
|
||||||
|
private Color activeSearchHighlightColor;
|
||||||
private Color searchHighlightColor;
|
private Color searchHighlightColor;
|
||||||
private SearchLocation currentSearchLocation;
|
|
||||||
|
private DecompilerSearchResults currentSearchResults;
|
||||||
|
|
||||||
private DecompileData decompileData = new EmptyDecompileData("No Function");
|
private DecompileData decompileData = new EmptyDecompileData("No Function");
|
||||||
private final DecompilerClipboardProvider clipboard;
|
private final DecompilerClipboardProvider clipboard;
|
||||||
@@ -145,6 +148,7 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field
|
|||||||
|
|
||||||
decompilerHoverProvider = new DecompilerHoverProvider();
|
decompilerHoverProvider = new DecompilerHoverProvider();
|
||||||
|
|
||||||
|
activeSearchHighlightColor = options.getActiveSearchHighlightColor();
|
||||||
searchHighlightColor = options.getSearchHighlightColor();
|
searchHighlightColor = options.getSearchHighlightColor();
|
||||||
currentVariableHighlightColor = options.getCurrentVariableHighlightColor();
|
currentVariableHighlightColor = options.getCurrentVariableHighlightColor();
|
||||||
middleMouseHighlightColor = options.getMiddleMouseHighlightColor();
|
middleMouseHighlightColor = options.getMiddleMouseHighlightColor();
|
||||||
@@ -535,7 +539,10 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field
|
|||||||
}
|
}
|
||||||
|
|
||||||
// don't highlight search results across functions
|
// don't highlight search results across functions
|
||||||
currentSearchLocation = null;
|
if (currentSearchResults != null) {
|
||||||
|
currentSearchResults.decompilerUpdated();
|
||||||
|
currentSearchResults = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (function != null) {
|
if (function != null) {
|
||||||
highlightController.reapplyAllHighlights(function);
|
highlightController.reapplyAllHighlights(function);
|
||||||
@@ -1105,13 +1112,32 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSearchResults(SearchLocation searchLocation) {
|
public void clearSearchResults(DecompilerSearchResults searchResults) {
|
||||||
currentSearchLocation = searchLocation;
|
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();
|
repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
public DecompilerSearchLocation getSearchResults() {
|
public DecompilerSearchLocation getActiveSearchLocation() {
|
||||||
return (DecompilerSearchLocation) currentSearchLocation;
|
if (currentSearchResults == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
DecompilerSearchLocation location = currentSearchResults.getActiveLocation();
|
||||||
|
return location;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Color getCurrentVariableHighlightColor() {
|
public Color getCurrentVariableHighlightColor() {
|
||||||
@@ -1370,23 +1396,30 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Highlight[] createHighlights(Field field, String text, int cursorTextOffset) {
|
public Highlight[] createHighlights(Field field, String text, int cursorTextOffset) {
|
||||||
if (currentSearchLocation == null) {
|
if (currentSearchResults == null) {
|
||||||
return new Highlight[0];
|
return new Highlight[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
ClangTextField cField = (ClangTextField) field;
|
ClangTextField cField = (ClangTextField) field;
|
||||||
int highlightLine = cField.getLineNumber();
|
int lineNumber = cField.getLineNumber();
|
||||||
|
Map<Integer, List<DecompilerSearchLocation>> locationsByLine =
|
||||||
FieldLocation searchCursorLocation =
|
currentSearchResults.getLocationsByLine();
|
||||||
((DecompilerSearchLocation) currentSearchLocation).getFieldLocation();
|
List<DecompilerSearchLocation> locationsOnLine = locationsByLine.get(lineNumber);
|
||||||
int searchLineNumber = searchCursorLocation.getIndex().intValue() + 1;
|
if (locationsOnLine.isEmpty()) {
|
||||||
if (highlightLine != searchLineNumber) {
|
|
||||||
// only highlight the match on the actual line
|
|
||||||
return new Highlight[0];
|
return new Highlight[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Highlight[] { new Highlight(currentSearchLocation.getStartIndexInclusive(),
|
DecompilerSearchLocation activeLocation = currentSearchResults.getActiveLocation();
|
||||||
currentSearchLocation.getEndIndexInclusive(), searchHighlightColor) };
|
List<Highlight> highlights = new ArrayList<>();
|
||||||
|
for (DecompilerSearchLocation location : locationsOnLine) {
|
||||||
|
Color c =
|
||||||
|
location == activeLocation ? activeSearchHighlightColor : searchHighlightColor;
|
||||||
|
int start = location.getStartIndexInclusive();
|
||||||
|
int end = location.getEndIndexInclusive();
|
||||||
|
highlights.add(new Highlight(start, end, c));
|
||||||
|
}
|
||||||
|
|
||||||
|
return highlights.toArray(Highlight[]::new);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
/* ###
|
/* ###
|
||||||
* IP: GHIDRA
|
* IP: GHIDRA
|
||||||
* REVIEWED: YES
|
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* 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) {
|
public void setOffset(int offset) {
|
||||||
location.col += offset;
|
location.col += offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return location.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,22 +18,20 @@ package ghidra.app.plugin.core.decompile.actions;
|
|||||||
import docking.widgets.CursorPosition;
|
import docking.widgets.CursorPosition;
|
||||||
import docking.widgets.SearchLocation;
|
import docking.widgets.SearchLocation;
|
||||||
import docking.widgets.fieldpanel.support.FieldLocation;
|
import docking.widgets.fieldpanel.support.FieldLocation;
|
||||||
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext;
|
import docking.widgets.search.SearchLocationContext;
|
||||||
|
|
||||||
public class DecompilerSearchLocation extends SearchLocation {
|
public class DecompilerSearchLocation extends SearchLocation {
|
||||||
|
|
||||||
private final FieldLocation fieldLocation;
|
private final FieldLocation fieldLocation;
|
||||||
private String textLine;
|
private String textLine;
|
||||||
private LocationReferenceContext context;
|
|
||||||
|
|
||||||
public DecompilerSearchLocation(FieldLocation fieldLocation, int startIndexInclusive,
|
public DecompilerSearchLocation(FieldLocation fieldLocation, int startIndexInclusive,
|
||||||
int endIndexInclusive, String searchText, boolean forwardDirection, String textLine,
|
int endIndexInclusive, String text, boolean forwardDirection, String textLine,
|
||||||
LocationReferenceContext context) {
|
int lineNumber, SearchLocationContext context) {
|
||||||
|
|
||||||
super(startIndexInclusive, endIndexInclusive, searchText, forwardDirection);
|
super(startIndexInclusive, endIndexInclusive, text, lineNumber, context);
|
||||||
this.fieldLocation = fieldLocation;
|
this.fieldLocation = fieldLocation;
|
||||||
this.textLine = textLine;
|
this.textLine = textLine;
|
||||||
this.context = context;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public FieldLocation getFieldLocation() {
|
public FieldLocation getFieldLocation() {
|
||||||
@@ -44,10 +42,6 @@ public class DecompilerSearchLocation extends SearchLocation {
|
|||||||
return textLine;
|
return textLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LocationReferenceContext getContext() {
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CursorPosition getCursorPosition() {
|
public CursorPosition getCursorPosition() {
|
||||||
return new DecompilerCursorPosition(fieldLocation);
|
return new DecompilerCursorPosition(fieldLocation);
|
||||||
@@ -57,4 +51,15 @@ public class DecompilerSearchLocation extends SearchLocation {
|
|||||||
protected String fieldsToString() {
|
protected String fieldsToString() {
|
||||||
return super.fieldsToString() + ", fieldLocation=" + fieldLocation;
|
return super.fieldsToString() + ", fieldLocation=" + fieldLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean contains(FieldLocation other) {
|
||||||
|
int line = getLineNumber();
|
||||||
|
int otherLine = other.getIndex().intValue() + 1; // +1 for zero based
|
||||||
|
if (line != otherLine) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int col = other.getCol();
|
||||||
|
return contains(col);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
/* ###
|
||||||
|
* IP: GHIDRA
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package ghidra.app.plugin.core.decompile.actions;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import docking.widgets.SearchLocation;
|
||||||
|
import docking.widgets.fieldpanel.support.FieldLocation;
|
||||||
|
import docking.widgets.search.SearchResults;
|
||||||
|
import ghidra.app.decompiler.component.DecompilerController;
|
||||||
|
import ghidra.app.decompiler.component.DecompilerPanel;
|
||||||
|
import ghidra.program.model.listing.Function;
|
||||||
|
import ghidra.program.model.listing.Program;
|
||||||
|
import ghidra.program.util.ProgramLocation;
|
||||||
|
import ghidra.util.exception.CancelledException;
|
||||||
|
import ghidra.util.task.TaskMonitor;
|
||||||
|
import ghidra.util.worker.Worker;
|
||||||
|
|
||||||
|
public class DecompilerSearchResults extends SearchResults {
|
||||||
|
|
||||||
|
// the location when the search was performed; used to know when the function has changed
|
||||||
|
private ProgramLocation programLocation;
|
||||||
|
private DecompilerPanel decompilerPanel;
|
||||||
|
private String searchText;
|
||||||
|
private List<SearchLocation> searchLocations;
|
||||||
|
private Map<Integer, List<DecompilerSearchLocation>> locationsByLine;
|
||||||
|
private TreeMap<LinePosition, DecompilerSearchLocation> matchesByPosition = new TreeMap<>();
|
||||||
|
|
||||||
|
private DecompilerSearchLocation activeLocation;
|
||||||
|
|
||||||
|
DecompilerSearchResults(Worker worker, DecompilerPanel decompilerPanel, String searchText,
|
||||||
|
List<SearchLocation> searchLocations) {
|
||||||
|
super(worker);
|
||||||
|
this.decompilerPanel = decompilerPanel;
|
||||||
|
this.searchText = searchText;
|
||||||
|
|
||||||
|
this.searchLocations = searchLocations;
|
||||||
|
this.programLocation = decompilerPanel.getCurrentLocation();
|
||||||
|
|
||||||
|
for (SearchLocation location : searchLocations) {
|
||||||
|
int line = location.getLineNumber();
|
||||||
|
int col = location.getStartIndexInclusive();
|
||||||
|
LinePosition lp = new LinePosition(line, col);
|
||||||
|
DecompilerSearchLocation dsl = (DecompilerSearchLocation) location;
|
||||||
|
matchesByPosition.put(lp, dsl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
DecompilerController controller = decompilerPanel.getController();
|
||||||
|
Function function = controller.getFunction();
|
||||||
|
return function.getName() + "()";
|
||||||
|
}
|
||||||
|
|
||||||
|
ProgramLocation getDecompileLocation() {
|
||||||
|
return programLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isInvalid(String otherSearchText) {
|
||||||
|
if (isDifferentFunction()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !searchText.equals(otherSearchText);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return searchLocations.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SearchLocation> getLocations() {
|
||||||
|
return searchLocations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<Integer, List<DecompilerSearchLocation>> getLocationsByLine() {
|
||||||
|
if (locationsByLine == null) {
|
||||||
|
locationsByLine = searchLocations.stream()
|
||||||
|
.map(l -> (DecompilerSearchLocation) l)
|
||||||
|
.collect(Collectors.groupingBy(l -> l.getLineNumber()));
|
||||||
|
}
|
||||||
|
return locationsByLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDifferentFunction() {
|
||||||
|
return !decompilerPanel.containsLocation(programLocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isMyFunction() {
|
||||||
|
return decompilerPanel.containsLocation(programLocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DecompilerSearchLocation getContainingLocation(FieldLocation fieldLocation,
|
||||||
|
boolean searchForward) {
|
||||||
|
|
||||||
|
// getNextLocation() will find the next matching location, starting at the given field
|
||||||
|
// location. The next location may or may not actually contain the given field location.
|
||||||
|
DecompilerSearchLocation nextLocation = getNextLocation(fieldLocation, searchForward);
|
||||||
|
if (nextLocation.contains(fieldLocation)) {
|
||||||
|
return nextLocation;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DecompilerSearchLocation getActiveLocation() {
|
||||||
|
return activeLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void installSearchResults() {
|
||||||
|
if (isDifferentFunction()) {
|
||||||
|
return; // a different function was decompiled while we were running
|
||||||
|
}
|
||||||
|
decompilerPanel.setSearchResults(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearSearchResults() {
|
||||||
|
decompilerPanel.clearSearchResults(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void decompilerUpdated() {
|
||||||
|
// The decompiler has updated. It may have been upon our request. If not, deactivate.
|
||||||
|
if (isDifferentFunction()) {
|
||||||
|
deactivate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deactivate() {
|
||||||
|
FindJob job = new SwingJob(this::clearSearchResults);
|
||||||
|
runJob(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void activate() {
|
||||||
|
FindJob job = createActivationJob().thenRunSwing(this::installSearchResults);
|
||||||
|
runJob(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setActiveLocation(SearchLocation location) {
|
||||||
|
|
||||||
|
if (activeLocation == location) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeLocation = (DecompilerSearchLocation) location;
|
||||||
|
if (location == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// activate() will set the active search location
|
||||||
|
activate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ActivationJob createActivationJob() {
|
||||||
|
if (isMyFunction()) {
|
||||||
|
return createFinishedActivationJob(); // nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
return (ActivationJob) new ActivateFunctionJob()
|
||||||
|
.thenWait(this::isMyFunction, Duration.ofSeconds(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ActivationJob createFinishedActivationJob() {
|
||||||
|
return new ActivationJob();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispose() {
|
||||||
|
setActiveLocation(null);
|
||||||
|
decompilerPanel.clearSearchResults(this);
|
||||||
|
searchLocations.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
DecompilerSearchLocation getNextLocation(FieldLocation startLocation,
|
||||||
|
boolean searchForward) {
|
||||||
|
|
||||||
|
Entry<LinePosition, DecompilerSearchLocation> entry;
|
||||||
|
int line = startLocation.getIndex().intValue() + 1; // +1 for zero based
|
||||||
|
int col = startLocation.getCol();
|
||||||
|
LinePosition lp = new LinePosition(line, col);
|
||||||
|
if (searchForward) {
|
||||||
|
entry = matchesByPosition.ceilingEntry(lp);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
entry = matchesByPosition.floorEntry(lp);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry == null) {
|
||||||
|
return null; // no more matches in the current direction
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
//=================================================================================================
|
||||||
|
// Inner Classes
|
||||||
|
//=================================================================================================
|
||||||
|
|
||||||
|
private class ActivateFunctionJob extends ActivationJob {
|
||||||
|
@Override
|
||||||
|
protected void doRun(TaskMonitor monitor) throws CancelledException {
|
||||||
|
if (isMyFunction()) {
|
||||||
|
return; // nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
DecompilerController controller = decompilerPanel.getController();
|
||||||
|
Program program = programLocation.getProgram();
|
||||||
|
controller.refreshDisplay(program, programLocation, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record LinePosition(int line, int col) implements Comparable<LinePosition> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(LinePosition other) {
|
||||||
|
|
||||||
|
int result = line - other.line;
|
||||||
|
if (result != 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return col - other.col;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -15,27 +15,31 @@
|
|||||||
*/
|
*/
|
||||||
package ghidra.app.plugin.core.decompile.actions;
|
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.function.Function;
|
||||||
import java.util.regex.*;
|
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.field.Field;
|
||||||
import docking.widgets.fieldpanel.support.FieldLocation;
|
import docking.widgets.fieldpanel.support.FieldLocation;
|
||||||
import docking.widgets.fieldpanel.support.RowColLocation;
|
import docking.widgets.fieldpanel.support.RowColLocation;
|
||||||
|
import docking.widgets.search.*;
|
||||||
import ghidra.app.decompiler.component.ClangTextField;
|
import ghidra.app.decompiler.component.ClangTextField;
|
||||||
import ghidra.app.decompiler.component.DecompilerPanel;
|
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.Msg;
|
||||||
import ghidra.util.UserSearchUtils;
|
import ghidra.util.UserSearchUtils;
|
||||||
|
import ghidra.util.worker.Worker;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link FindDialogSearcher} for searching the text of the decompiler window.
|
* A {@link FindDialogSearcher} for searching the text of the decompiler window.
|
||||||
*/
|
*/
|
||||||
public class DecompilerSearcher implements FindDialogSearcher {
|
public class DecompilerSearcher implements FindDialogSearcher {
|
||||||
|
|
||||||
|
private Worker worker = Worker.createGuiWorker();
|
||||||
private DecompilerPanel decompilerPanel;
|
private DecompilerPanel decompilerPanel;
|
||||||
|
private DecompilerSearchResults searchResults;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
@@ -76,98 +80,39 @@ public class DecompilerSearcher implements FindDialogSearcher {
|
|||||||
return new DecompilerCursorPosition(fieldLocation);
|
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
|
@Override
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
clearHighlights();
|
decompilerPanel.setSearchResults(null);
|
||||||
|
|
||||||
|
if (searchResults != null) {
|
||||||
|
searchResults.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void updateSearchResults(String text, boolean useRegex) {
|
||||||
public SearchLocation search(String text, CursorPosition position, boolean searchForward,
|
if (searchResults != null) {
|
||||||
boolean useRegex) {
|
if (!searchResults.isInvalid(text)) {
|
||||||
DecompilerCursorPosition decompilerCursorPosition = (DecompilerCursorPosition) position;
|
|
||||||
FieldLocation startLocation =
|
|
||||||
getNextSearchStartLocation(decompilerCursorPosition, searchForward);
|
|
||||||
return doFind(text, startLocation, searchForward, useRegex);
|
|
||||||
}
|
|
||||||
|
|
||||||
private FieldLocation getNextSearchStartLocation(
|
// the current results are still valid; ensure the highlights are still active
|
||||||
DecompilerCursorPosition decompilerCursorPosition, boolean searchForward) {
|
searchResults.activate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
FieldLocation startLocation = decompilerCursorPosition.getFieldLocation();
|
searchResults.dispose();
|
||||||
DecompilerSearchLocation currentSearchLocation = decompilerPanel.getSearchResults();
|
searchResults = null;
|
||||||
if (currentSearchLocation == null) {
|
|
||||||
return startLocation; // nothing to do; no prior search hit
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
searchResults = doSearch(text, useRegex);
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//=================================================================================================
|
private DecompilerSearchResults doSearch(String searchText, boolean isRegex) {
|
||||||
// Search Methods
|
|
||||||
//=================================================================================================
|
|
||||||
|
|
||||||
@Override
|
Pattern pattern = createPattern(searchText, isRegex);
|
||||||
public List<SearchLocation> searchAll(String searchString, boolean isRegex) {
|
Function<String, SearchMatch> forwardMatcher = createForwardMatchFunction(pattern);
|
||||||
|
|
||||||
Pattern pattern = createPattern(searchString, isRegex);
|
|
||||||
Function<String, SearchMatch> function = createForwardMatchFunction(pattern);
|
|
||||||
FieldLocation start = new FieldLocation();
|
FieldLocation start = new FieldLocation();
|
||||||
|
|
||||||
List<SearchLocation> results = new ArrayList<>();
|
List<SearchLocation> results = new ArrayList<>();
|
||||||
DecompilerSearchLocation searchLocation = findNext(function, searchString, start);
|
DecompilerSearchLocation searchLocation = findNext(forwardMatcher, searchText, start);
|
||||||
while (searchLocation != null) {
|
while (searchLocation != null) {
|
||||||
results.add(searchLocation);
|
results.add(searchLocation);
|
||||||
|
|
||||||
@@ -177,24 +122,62 @@ public class DecompilerSearcher implements FindDialogSearcher {
|
|||||||
int row = 0; // there is only 1 row
|
int row = 0; // there is only 1 row
|
||||||
int col = last.getCol() + 1; // move over one char to handle sub-matches
|
int col = last.getCol() + 1; // move over one char to handle sub-matches
|
||||||
start = new FieldLocation(line, field, row, col);
|
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) {
|
updateSearchResults(text, useRegex);
|
||||||
Function<String, SearchMatch> function = createForwardMatchFunction(pattern);
|
|
||||||
return findNext(function, searchString, currentLocation);
|
DecompilerCursorPosition cursorPosition = (DecompilerCursorPosition) position;
|
||||||
|
FieldLocation startLocation = getNextSearchStartLocation(cursorPosition, searchForward);
|
||||||
|
DecompilerSearchLocation location =
|
||||||
|
searchResults.getNextLocation(startLocation, searchForward);
|
||||||
|
if (location == null) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Function<String, SearchMatch> reverse = createReverseMatchFunction(pattern);
|
searchResults.setActiveLocation(location);
|
||||||
return findPrevious(reverse, searchString, currentLocation);
|
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) {
|
private Pattern createPattern(String searchString, boolean isRegex) {
|
||||||
@@ -230,35 +213,6 @@ public class DecompilerSearcher implements FindDialogSearcher {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Function<String, SearchMatch> createReverseMatchFunction(Pattern pattern) {
|
|
||||||
|
|
||||||
return textLine -> {
|
|
||||||
|
|
||||||
Matcher matcher = pattern.matcher(textLine);
|
|
||||||
if (!matcher.find()) {
|
|
||||||
return SearchMatch.NO_MATCH;
|
|
||||||
}
|
|
||||||
|
|
||||||
int start = matcher.start();
|
|
||||||
int end = matcher.end();
|
|
||||||
|
|
||||||
// Since the matcher can only match from the start to end of line, we need to find all
|
|
||||||
// matches and then take the last match
|
|
||||||
|
|
||||||
// Setting the region to one character past the previous match allows repeated matches
|
|
||||||
// within a match. The default behavior of the matcher is to start the match after
|
|
||||||
// the previous match found by find().
|
|
||||||
matcher.region(start + 1, textLine.length());
|
|
||||||
while (matcher.find()) {
|
|
||||||
start = matcher.start();
|
|
||||||
end = matcher.end();
|
|
||||||
matcher.region(start + 1, textLine.length());
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SearchMatch(start, end, textLine);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private DecompilerSearchLocation findNext(Function<String, SearchMatch> matcher,
|
private DecompilerSearchLocation findNext(Function<String, SearchMatch> matcher,
|
||||||
String searchString, FieldLocation currentLocation) {
|
String searchString, FieldLocation currentLocation) {
|
||||||
|
|
||||||
@@ -288,15 +242,16 @@ public class DecompilerSearcher implements FindDialogSearcher {
|
|||||||
FieldLineLocation lineInfo = getFieldIndexFromOffset(match.start, field);
|
FieldLineLocation lineInfo = getFieldIndexFromOffset(match.start, field);
|
||||||
FieldLocation fieldLocation =
|
FieldLocation fieldLocation =
|
||||||
new FieldLocation(i, lineInfo.fieldNumber(), 0, lineInfo.column());
|
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,
|
return new DecompilerSearchLocation(fieldLocation, match.start, match.end - 1,
|
||||||
searchString, true, field.getText(), context);
|
searchString, true, field.getText(), lineNumber, context);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private LocationReferenceContext createContext(String line, SearchMatch match) {
|
private SearchLocationContext createContext(String line, SearchMatch match) {
|
||||||
LocationReferenceContextBuilder builder = new LocationReferenceContextBuilder();
|
SearchLocationContextBuilder builder = new SearchLocationContextBuilder();
|
||||||
int start = match.start;
|
int start = match.start;
|
||||||
int end = match.end;
|
int end = match.end;
|
||||||
builder.append(line.substring(0, start));
|
builder.append(line.substring(0, start));
|
||||||
@@ -308,29 +263,6 @@ public class DecompilerSearcher implements FindDialogSearcher {
|
|||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private DecompilerSearchLocation findPrevious(Function<String, SearchMatch> matcher,
|
|
||||||
String searchString, FieldLocation currentLocation) {
|
|
||||||
|
|
||||||
List<Field> fields = decompilerPanel.getFields();
|
|
||||||
int line = currentLocation.getIndex().intValue();
|
|
||||||
for (int i = line; i >= 0; i--) {
|
|
||||||
ClangTextField field = (ClangTextField) fields.get(i);
|
|
||||||
String textLine = substring(field, (i == line) ? currentLocation : null, false);
|
|
||||||
SearchMatch match = matcher.apply(textLine);
|
|
||||||
if (match == SearchMatch.NO_MATCH) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
FieldLineLocation lineInfo = getFieldIndexFromOffset(match.start, field);
|
|
||||||
FieldLocation fieldLocation =
|
|
||||||
new FieldLocation(i, lineInfo.fieldNumber(), 0, lineInfo.column());
|
|
||||||
LocationReferenceContext context = createContext(field.getText(), match);
|
|
||||||
return new DecompilerSearchLocation(fieldLocation, match.start, match.end - 1,
|
|
||||||
searchString, false, field.getText(), context);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String substring(ClangTextField textField, FieldLocation location,
|
private String substring(ClangTextField textField, FieldLocation location,
|
||||||
boolean forwardSearch) {
|
boolean forwardSearch) {
|
||||||
|
|
||||||
@@ -364,9 +296,10 @@ public class DecompilerSearcher implements FindDialogSearcher {
|
|||||||
private FieldLineLocation getFieldIndexFromOffset(int screenOffset, ClangTextField textField) {
|
private FieldLineLocation getFieldIndexFromOffset(int screenOffset, ClangTextField textField) {
|
||||||
|
|
||||||
RowColLocation rowColLocation = textField.textOffsetToScreenLocation(screenOffset);
|
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
|
// 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 {
|
private static class SearchMatch {
|
||||||
@@ -390,5 +323,5 @@ public class DecompilerSearcher implements FindDialogSearcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private record FieldLineLocation(int fieldNumber, int column) {}
|
private record FieldLineLocation(int fieldNumber, int lineNumber, int column) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,11 @@ public class FindAction extends AbstractDecompilerAction {
|
|||||||
dialog.setSearchText(text);
|
dialog.setSearchText(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dialog.isShowing()) {
|
||||||
|
dialog.toFront();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// show over the root frame, so the user can still see the Decompiler window
|
// show over the root frame, so the user can still see the Decompiler window
|
||||||
context.getTool().showDialog(dialog);
|
context.getTool().showDialog(dialog);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
@@ -340,7 +340,7 @@ public class DecompilerFindDialogTest extends AbstractDecompilerTest {
|
|||||||
assertCurrentLocation(line, column);
|
assertCurrentLocation(line, column);
|
||||||
|
|
||||||
DecompilerPanel panel = getDecompilerPanel();
|
DecompilerPanel panel = getDecompilerPanel();
|
||||||
DecompilerSearchLocation searchResults = panel.getSearchResults();
|
DecompilerSearchLocation searchResults = panel.getActiveSearchLocation();
|
||||||
FieldLocation searchCursorLocation = searchResults.getFieldLocation();
|
FieldLocation searchCursorLocation = searchResults.getFieldLocation();
|
||||||
int searchLineNumber = searchCursorLocation.getIndex().intValue() + 1;
|
int searchLineNumber = searchCursorLocation.getIndex().intValue() + 1;
|
||||||
assertEquals("Search result is on the wrong line", line, searchLineNumber);
|
assertEquals("Search result is on the wrong line", line, searchLineNumber);
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ package ghidra.app.extension.datatype.finder;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import docking.widgets.search.SearchLocationContext;
|
||||||
|
import docking.widgets.search.SearchLocationContextBuilder;
|
||||||
import ghidra.app.decompiler.*;
|
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.DataTypeReference;
|
||||||
import ghidra.app.services.FieldMatcher;
|
import ghidra.app.services.FieldMatcher;
|
||||||
import ghidra.program.model.address.Address;
|
import ghidra.program.model.address.Address;
|
||||||
@@ -87,14 +87,14 @@ public abstract class DecompilerReference {
|
|||||||
return line;
|
return line;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected LocationReferenceContext getContext() {
|
protected SearchLocationContext getContext() {
|
||||||
LocationReferenceContext context = getContext(variable);
|
SearchLocationContext context = getContext(variable);
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected LocationReferenceContext getContext(DecompilerVariable var) {
|
protected SearchLocationContext getContext(DecompilerVariable var) {
|
||||||
|
|
||||||
LocationReferenceContextBuilder builder = new LocationReferenceContextBuilder();
|
SearchLocationContextBuilder builder = new SearchLocationContextBuilder();
|
||||||
builder.append(line.getLineNumber() + ": ");
|
builder.append(line.getLineNumber() + ": ");
|
||||||
List<ClangToken> tokens = line.getAllTokens();
|
List<ClangToken> tokens = line.getAllTokens();
|
||||||
for (ClangToken token : tokens) {
|
for (ClangToken token : tokens) {
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ package ghidra.app.extension.datatype.finder;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import docking.widgets.search.SearchLocationContext;
|
||||||
import ghidra.app.decompiler.*;
|
import ghidra.app.decompiler.*;
|
||||||
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext;
|
|
||||||
import ghidra.app.services.DataTypeReference;
|
import ghidra.app.services.DataTypeReference;
|
||||||
import ghidra.app.services.FieldMatcher;
|
import ghidra.app.services.FieldMatcher;
|
||||||
import ghidra.program.model.address.Address;
|
import ghidra.program.model.address.Address;
|
||||||
@@ -240,7 +240,7 @@ public class VariableAccessDR extends DecompilerReference {
|
|||||||
protected DataTypeReference createReference(DecompilerVariable var) {
|
protected DataTypeReference createReference(DecompilerVariable var) {
|
||||||
|
|
||||||
DataType dataType = var.getDataType();
|
DataType dataType = var.getDataType();
|
||||||
LocationReferenceContext context = getContext(var);
|
SearchLocationContext context = getContext(var);
|
||||||
Function function = var.getFunction();
|
Function function = var.getFunction();
|
||||||
Address address = getAddress(var);
|
Address address = getAddress(var);
|
||||||
return new DataTypeReference(dataType, null, function, address, context);
|
return new DataTypeReference(dataType, null, function, address, context);
|
||||||
@@ -248,16 +248,16 @@ public class VariableAccessDR extends DecompilerReference {
|
|||||||
|
|
||||||
private DataTypeReference createReference(DecompilerVariable var, DecompilerVariable field) {
|
private DataTypeReference createReference(DecompilerVariable var, DecompilerVariable field) {
|
||||||
DataType dataType = var.getDataType();
|
DataType dataType = var.getDataType();
|
||||||
LocationReferenceContext context = getContext(var);
|
SearchLocationContext context = getContext(var);
|
||||||
Function function = var.getFunction();
|
Function function = var.getFunction();
|
||||||
Address address = getAddress(var);
|
Address address = getAddress(var);
|
||||||
return new DataTypeReference(dataType, field.getName(), function, address, context);
|
return new DataTypeReference(dataType, field.getName(), function, address, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected LocationReferenceContext getContext(DecompilerVariable var) {
|
protected SearchLocationContext getContext(DecompilerVariable var) {
|
||||||
DecompilerVariable field = findFieldFor(var);
|
DecompilerVariable field = findFieldFor(var);
|
||||||
LocationReferenceContext context = super.getContext(field);
|
SearchLocationContext context = super.getContext(field);
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* 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 java.util.List;
|
||||||
|
|
||||||
|
import docking.widgets.search.SearchLocationContext;
|
||||||
import ghidra.app.decompiler.*;
|
import ghidra.app.decompiler.*;
|
||||||
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext;
|
|
||||||
import ghidra.app.services.DataTypeReference;
|
import ghidra.app.services.DataTypeReference;
|
||||||
import ghidra.app.services.FieldMatcher;
|
import ghidra.app.services.FieldMatcher;
|
||||||
import ghidra.program.model.address.Address;
|
import ghidra.program.model.address.Address;
|
||||||
@@ -74,7 +74,7 @@ public abstract class VariableDR extends DecompilerReference {
|
|||||||
String fieldName = fieldMatcher.getFieldName();
|
String fieldName = fieldMatcher.getFieldName();
|
||||||
Function function = getFunction();
|
Function function = getFunction();
|
||||||
Address address = getAddress();
|
Address address = getAddress();
|
||||||
LocationReferenceContext context = getContext();
|
SearchLocationContext context = getContext();
|
||||||
results.add(new DataTypeReference(dataType, fieldName, function, address, context));
|
results.add(new DataTypeReference(dataType, fieldName, function, address, context));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ import java.util.function.Consumer;
|
|||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import docking.widgets.search.SearchLocationContext;
|
||||||
|
import docking.widgets.search.SearchLocationContextBuilder;
|
||||||
import generic.json.Json;
|
import generic.json.Json;
|
||||||
import ghidra.app.decompiler.*;
|
import ghidra.app.decompiler.*;
|
||||||
import ghidra.app.decompiler.component.DecompilerUtils;
|
import ghidra.app.decompiler.component.DecompilerUtils;
|
||||||
import ghidra.app.decompiler.parallel.*;
|
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.AddressSet;
|
||||||
import ghidra.program.model.address.AddressSpace;
|
import ghidra.program.model.address.AddressSpace;
|
||||||
import ghidra.program.model.listing.*;
|
import ghidra.program.model.listing.*;
|
||||||
@@ -246,15 +246,15 @@ public class DecompilerTextFinder {
|
|||||||
TextLine firstLine = lineMatches.get(0);
|
TextLine firstLine = lineMatches.get(0);
|
||||||
int lineNumber = firstLine.getLineNumber();
|
int lineNumber = firstLine.getLineNumber();
|
||||||
AddressSet addresses = getAddresses(function, firstLine.getCLine());
|
AddressSet addresses = getAddresses(function, firstLine.getCLine());
|
||||||
LocationReferenceContext context = createMatchContext(lineMatches);
|
SearchLocationContext context = createMatchContext(lineMatches);
|
||||||
TextMatch match =
|
TextMatch match =
|
||||||
new TextMatch(function, addresses, lineNumber, searchText, context, true);
|
new TextMatch(function, addresses, lineNumber, searchText, context, true);
|
||||||
callback.accept(match);
|
callback.accept(match);
|
||||||
}
|
}
|
||||||
|
|
||||||
private LocationReferenceContext createMatchContext(List<TextLine> matches) {
|
private SearchLocationContext createMatchContext(List<TextLine> matches) {
|
||||||
|
|
||||||
LocationReferenceContextBuilder builder = new LocationReferenceContextBuilder();
|
SearchLocationContextBuilder builder = new SearchLocationContextBuilder();
|
||||||
for (TextLine line : matches) {
|
for (TextLine line : matches) {
|
||||||
if (!builder.isEmpty()) {
|
if (!builder.isEmpty()) {
|
||||||
builder.newline();
|
builder.newline();
|
||||||
@@ -279,7 +279,7 @@ public class DecompilerTextFinder {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LocationReferenceContextBuilder builder = new LocationReferenceContextBuilder();
|
SearchLocationContextBuilder builder = new SearchLocationContextBuilder();
|
||||||
|
|
||||||
int start = matcher.start();
|
int start = matcher.start();
|
||||||
int end = matcher.end();
|
int end = matcher.end();
|
||||||
@@ -291,7 +291,7 @@ public class DecompilerTextFinder {
|
|||||||
|
|
||||||
int lineNumber = line.getLineNumber();
|
int lineNumber = line.getLineNumber();
|
||||||
AddressSet addresses = getAddresses(function, line);
|
AddressSet addresses = getAddresses(function, line);
|
||||||
LocationReferenceContext context = builder.build();
|
SearchLocationContext context = builder.build();
|
||||||
TextMatch match =
|
TextMatch match =
|
||||||
new TextMatch(function, addresses, lineNumber, searchText, context, false);
|
new TextMatch(function, addresses, lineNumber, searchText, context, false);
|
||||||
callback.accept(match);
|
callback.accept(match);
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import docking.widgets.search.SearchLocationContext;
|
||||||
import docking.widgets.table.*;
|
import docking.widgets.table.*;
|
||||||
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext;
|
|
||||||
import ghidra.docking.settings.Settings;
|
import ghidra.docking.settings.Settings;
|
||||||
import ghidra.framework.plugintool.ServiceProvider;
|
import ghidra.framework.plugintool.ServiceProvider;
|
||||||
import ghidra.program.model.address.Address;
|
import ghidra.program.model.address.Address;
|
||||||
@@ -142,12 +142,12 @@ public class DecompilerTextFinderTableModel extends GhidraProgramTableModel<Text
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class ContextTableColumn
|
private class ContextTableColumn
|
||||||
extends AbstractProgramBasedDynamicTableColumn<TextMatch, LocationReferenceContext> {
|
extends AbstractProgramBasedDynamicTableColumn<TextMatch, SearchLocationContext> {
|
||||||
|
|
||||||
private ContextCellRenderer renderer = new ContextCellRenderer();
|
private ContextCellRenderer renderer = new ContextCellRenderer();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LocationReferenceContext getValue(TextMatch rowObject, Settings settings, Program p,
|
public SearchLocationContext getValue(TextMatch rowObject, Settings settings, Program p,
|
||||||
ServiceProvider sp) throws IllegalArgumentException {
|
ServiceProvider sp) throws IllegalArgumentException {
|
||||||
return rowObject.getContext();
|
return rowObject.getContext();
|
||||||
}
|
}
|
||||||
@@ -158,13 +158,13 @@ public class DecompilerTextFinderTableModel extends GhidraProgramTableModel<Text
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public GColumnRenderer<LocationReferenceContext> getColumnRenderer() {
|
public GColumnRenderer<SearchLocationContext> getColumnRenderer() {
|
||||||
return renderer;
|
return renderer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ContextCellRenderer
|
private class ContextCellRenderer
|
||||||
extends AbstractGhidraColumnRenderer<LocationReferenceContext> {
|
extends AbstractGhidraColumnRenderer<SearchLocationContext> {
|
||||||
|
|
||||||
{
|
{
|
||||||
// the context uses html
|
// the context uses html
|
||||||
@@ -178,7 +178,7 @@ public class DecompilerTextFinderTableModel extends GhidraProgramTableModel<Text
|
|||||||
super.getTableCellRendererComponent(data);
|
super.getTableCellRendererComponent(data);
|
||||||
|
|
||||||
TextMatch match = (TextMatch) data.getRowObject();
|
TextMatch match = (TextMatch) data.getRowObject();
|
||||||
LocationReferenceContext context = match.getContext();
|
SearchLocationContext context = match.getContext();
|
||||||
String text;
|
String text;
|
||||||
if (match.isMultiLine()) {
|
if (match.isMultiLine()) {
|
||||||
// multi-line matches create visual noise when showing colors, as of much of the
|
// multi-line matches create visual noise when showing colors, as of much of the
|
||||||
@@ -193,7 +193,7 @@ public class DecompilerTextFinderTableModel extends GhidraProgramTableModel<Text
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getFilterString(LocationReferenceContext context, Settings settings) {
|
public String getFilterString(SearchLocationContext context, Settings settings) {
|
||||||
return context.getPlainText();
|
return context.getPlainText();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
package ghidra.app.plugin.core.search;
|
package ghidra.app.plugin.core.search;
|
||||||
|
|
||||||
|
import docking.widgets.search.SearchLocationContext;
|
||||||
import generic.json.Json;
|
import generic.json.Json;
|
||||||
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext;
|
|
||||||
import ghidra.program.model.address.Address;
|
import ghidra.program.model.address.Address;
|
||||||
import ghidra.program.model.address.AddressSet;
|
import ghidra.program.model.address.AddressSet;
|
||||||
import ghidra.program.model.listing.Function;
|
import ghidra.program.model.listing.Function;
|
||||||
@@ -28,14 +28,14 @@ public class TextMatch {
|
|||||||
|
|
||||||
private Function function;
|
private Function function;
|
||||||
private AddressSet addresses;
|
private AddressSet addresses;
|
||||||
private LocationReferenceContext context;
|
private SearchLocationContext context;
|
||||||
private int lineNumber;
|
private int lineNumber;
|
||||||
|
|
||||||
private String searchText;
|
private String searchText;
|
||||||
private boolean isMultiLine;
|
private boolean isMultiLine;
|
||||||
|
|
||||||
TextMatch(Function function, AddressSet addresses, int lineNumber, String searchText,
|
TextMatch(Function function, AddressSet addresses, int lineNumber, String searchText,
|
||||||
LocationReferenceContext context, boolean isMultiLine) {
|
SearchLocationContext context, boolean isMultiLine) {
|
||||||
this.function = function;
|
this.function = function;
|
||||||
this.addresses = addresses;
|
this.addresses = addresses;
|
||||||
this.lineNumber = lineNumber;
|
this.lineNumber = lineNumber;
|
||||||
@@ -48,7 +48,7 @@ public class TextMatch {
|
|||||||
return function;
|
return function;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LocationReferenceContext getContext() {
|
public SearchLocationContext getContext() {
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ src/main/resources/images/play.png||GHIDRA||||END|
|
|||||||
src/main/resources/images/preferences-system-windows.png||Tango Icons - Public Domain||||END|
|
src/main/resources/images/preferences-system-windows.png||Tango Icons - Public Domain||||END|
|
||||||
src/main/resources/images/software-update-available.png||Tango Icons - Public Domain|||tango icon set|END|
|
src/main/resources/images/software-update-available.png||Tango Icons - Public Domain|||tango icon set|END|
|
||||||
src/main/resources/images/table.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
|
src/main/resources/images/table.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
|
||||||
|
src/main/resources/images/table_delete.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
|
||||||
src/main/resources/images/table_relationship.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
|
src/main/resources/images/table_relationship.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
|
||||||
src/main/resources/images/tag.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
|
src/main/resources/images/tag.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
|
||||||
src/main/resources/images/text_lowercase.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
|
src/main/resources/images/text_lowercase.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END|
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ icon.widget.pathmanager.reset = trash-empty.png
|
|||||||
icon.widget.table.header.help = info_small.png
|
icon.widget.table.header.help = info_small.png
|
||||||
icon.widget.table.header.help.hovered = info_small_hover.png
|
icon.widget.table.header.help.hovered = info_small_hover.png
|
||||||
icon.widget.table.header.pending = icon.pending
|
icon.widget.table.header.pending = icon.pending
|
||||||
|
icon.widget.table.delete.row = table_delete.png
|
||||||
|
|
||||||
icon.widget.tabs.empty.small = empty8x16.png
|
icon.widget.tabs.empty.small = empty8x16.png
|
||||||
icon.widget.tabs.close = x.gif
|
icon.widget.tabs.close = x.gif
|
||||||
|
|||||||
@@ -318,14 +318,16 @@ class ComponentNode extends Node {
|
|||||||
ComponentPlaceholder placeholder = activeComponents.get(i);
|
ComponentPlaceholder placeholder = activeComponents.get(i);
|
||||||
DockableComponent c = placeholder.getComponent();
|
DockableComponent c = placeholder.getComponent();
|
||||||
c.setBorder(BorderFactory.createEmptyBorder());
|
c.setBorder(BorderFactory.createEmptyBorder());
|
||||||
String title = placeholder.getTitle();
|
|
||||||
|
// The renderer uses use the full title as the tooltip for the tab
|
||||||
|
String fullTitle = placeholder.getFullTitle();
|
||||||
String tabText = placeholder.getTabText();
|
String tabText = placeholder.getTabText();
|
||||||
|
|
||||||
final DockableComponent component = placeholder.getComponent();
|
DockableComponent component = placeholder.getComponent();
|
||||||
tabbedPane.add(component, title);
|
tabbedPane.add(fullTitle, component);
|
||||||
|
|
||||||
DockingTabRenderer tabRenderer =
|
DockingTabRenderer tabRenderer =
|
||||||
createTabRenderer(tabbedPane, placeholder, title, tabText, component);
|
createTabRenderer(tabbedPane, placeholder, fullTitle, tabText, component);
|
||||||
|
|
||||||
c.installDragDropTarget(tabbedPane);
|
c.installDragDropTarget(tabbedPane);
|
||||||
|
|
||||||
|
|||||||
@@ -448,6 +448,7 @@ public class ComponentPlaceholder {
|
|||||||
* @param newProvider the new provider
|
* @param newProvider the new provider
|
||||||
*/
|
*/
|
||||||
void setProvider(ComponentProvider newProvider) {
|
void setProvider(ComponentProvider newProvider) {
|
||||||
|
|
||||||
this.componentProvider = newProvider;
|
this.componentProvider = newProvider;
|
||||||
actions.clear();
|
actions.clear();
|
||||||
if (newProvider != null) {
|
if (newProvider != null) {
|
||||||
|
|||||||
@@ -923,7 +923,7 @@ public abstract class ComponentProvider implements HelpDescriptor, ActionContext
|
|||||||
*
|
*
|
||||||
* @param group the group for this provider.
|
* @param group the group for this provider.
|
||||||
*/
|
*/
|
||||||
protected void setWindowGroup(String group) {
|
public void setWindowGroup(String group) {
|
||||||
this.group = group;
|
this.group = group;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,28 +18,33 @@ package docking.help;
|
|||||||
import java.awt.Component;
|
import java.awt.Component;
|
||||||
import java.awt.Window;
|
import java.awt.Window;
|
||||||
import java.awt.event.*;
|
import java.awt.event.*;
|
||||||
import java.io.File;
|
import java.beans.PropertyChangeEvent;
|
||||||
|
import java.beans.PropertyChangeListener;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Enumeration;
|
import java.time.Duration;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
import javax.help.*;
|
import javax.help.*;
|
||||||
import javax.help.search.SearchEngine;
|
import javax.help.search.SearchEngine;
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
|
import javax.swing.text.Document;
|
||||||
|
|
||||||
import docking.DockingUtils;
|
import docking.DockingUtils;
|
||||||
import docking.DockingWindowManager;
|
import docking.DockingWindowManager;
|
||||||
import docking.actions.KeyBindingUtils;
|
import docking.actions.KeyBindingUtils;
|
||||||
import docking.widgets.FindDialog;
|
import docking.widgets.FindDialog;
|
||||||
import docking.widgets.TextComponentSearcher;
|
import docking.widgets.SearchLocation;
|
||||||
|
import docking.widgets.search.*;
|
||||||
import generic.util.WindowUtilities;
|
import generic.util.WindowUtilities;
|
||||||
import ghidra.util.exception.AssertException;
|
import ghidra.util.exception.AssertException;
|
||||||
|
import ghidra.util.task.TaskMonitor;
|
||||||
|
import ghidra.util.worker.Worker;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enables the Find Dialog for searching through the current page of a help document.
|
* Enables the Find Dialog for searching through the current page of a help document.
|
||||||
*/
|
*/
|
||||||
class HelpViewSearcher {
|
class HelpViewSearcher {
|
||||||
|
|
||||||
private static final String DIALOG_TITLE_PREFIX = "Whole Word Search in ";
|
|
||||||
private static final String FIND_ACTION_NAME = "find.action";
|
private static final String FIND_ACTION_NAME = "find.action";
|
||||||
|
|
||||||
private static KeyStroke FIND_KEYSTROKE =
|
private static KeyStroke FIND_KEYSTROKE =
|
||||||
@@ -59,24 +64,11 @@ class HelpViewSearcher {
|
|||||||
|
|
||||||
JHelpContentViewer contentViewer = jHelp.getContentViewer();
|
JHelpContentViewer contentViewer = jHelp.getContentViewer();
|
||||||
|
|
||||||
contentViewer.addHelpModelListener(e -> {
|
|
||||||
URL url = e.getURL();
|
|
||||||
if (!isValidHelpURL(url)) {
|
|
||||||
// invalid file--don't enable searching for it
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String file = url.getFile();
|
|
||||||
int separatorIndex = file.lastIndexOf(File.separator);
|
|
||||||
file = file.substring(separatorIndex + 1);
|
|
||||||
findDialog.setTitle(DIALOG_TITLE_PREFIX + file);
|
|
||||||
});
|
|
||||||
|
|
||||||
// note: see HTMLEditorKit$LinkController.mouseMoved() for inspiration
|
// note: see HTMLEditorKit$LinkController.mouseMoved() for inspiration
|
||||||
htmlEditorPane = getHTMLEditorPane(contentViewer);
|
htmlEditorPane = getHTMLEditorPane(contentViewer);
|
||||||
|
|
||||||
TextComponentSearcher searcher = new TextComponentSearcher(htmlEditorPane);
|
HtmlTextSearcher searcher = new HtmlTextSearcher(htmlEditorPane);
|
||||||
findDialog = new FindDialog(DIALOG_TITLE_PREFIX, searcher);
|
findDialog = new FindDialog("Help Find", searcher);
|
||||||
|
|
||||||
htmlEditorPane.addMouseListener(new MouseAdapter() {
|
htmlEditorPane.addMouseListener(new MouseAdapter() {
|
||||||
@Override
|
@Override
|
||||||
@@ -93,14 +85,6 @@ class HelpViewSearcher {
|
|||||||
// highlighter.addHighlight(0, 0, null)
|
// 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() {
|
private void grabSearchEngine() {
|
||||||
Enumeration<?> navigators = jHelp.getHelpNavigators();
|
Enumeration<?> navigators = jHelp.getHelpNavigators();
|
||||||
while (navigators.hasMoreElements()) {
|
while (navigators.hasMoreElements()) {
|
||||||
@@ -193,6 +177,156 @@ class HelpViewSearcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class HtmlTextSearcher extends TextComponentSearcher {
|
||||||
|
|
||||||
|
public HtmlTextSearcher(JEditorPane editorPane) {
|
||||||
|
super(editorPane);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HtmlSearchResults createSearchResults(
|
||||||
|
Worker worker, JEditorPane jEditorPane, String searchText,
|
||||||
|
TreeMap<Integer, TextComponentSearchLocation> matchesByPosition) {
|
||||||
|
|
||||||
|
HtmlSearchResults results = new HtmlSearchResults(worker, jEditorPane, searchText,
|
||||||
|
matchesByPosition);
|
||||||
|
|
||||||
|
TextHelpModel model = jHelp.getModel();
|
||||||
|
URL url = model.getCurrentURL();
|
||||||
|
results.setUrl(url);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class HtmlSearchResults extends TextComponentSearchResults {
|
||||||
|
|
||||||
|
private PageLoadedListener pageLoadListener;
|
||||||
|
private URL searchUrl;
|
||||||
|
|
||||||
|
// we use the document length to know when our page is finished loading on a reload
|
||||||
|
private int fullDocumentLength;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
HtmlSearchResults(Worker worker, JEditorPane editorPane, String searchText,
|
||||||
|
TreeMap<Integer, TextComponentSearchLocation> matchesByPosition) {
|
||||||
|
super(worker, editorPane, searchText, matchesByPosition);
|
||||||
|
|
||||||
|
pageLoadListener = new PageLoadedListener(this);
|
||||||
|
editorPane.addPropertyChangeListener("page", pageLoadListener);
|
||||||
|
|
||||||
|
Document doc = editorPane.getDocument();
|
||||||
|
fullDocumentLength = doc.getLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isInvalid(String otherSearchText) {
|
||||||
|
if (!isMyHelpPageShowing()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.isInvalid(otherSearchText);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isMyHelpPageShowing() {
|
||||||
|
TextHelpModel model = jHelp.getModel();
|
||||||
|
URL htmlViewerURL = model.getCurrentURL();
|
||||||
|
if (!Objects.equals(htmlViewerURL, searchUrl)) {
|
||||||
|
// the help does not have my page
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The document gets loaded asynchronously. Use the length to know when it is finished
|
||||||
|
// loaded. This will not work correctly when the lengths are the same between two
|
||||||
|
// documents, but that should be a low occurrence event.
|
||||||
|
Document doc = editorPane.getDocument();
|
||||||
|
int currentLength = doc.getLength();
|
||||||
|
return fullDocumentLength == currentLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadMyHelpPage(TaskMonitor m) {
|
||||||
|
if (isMyHelpPageShowing()) {
|
||||||
|
return; // no need to reload
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger the URL of the results to load and then activate the results
|
||||||
|
jHelp.setCurrentURL(searchUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start an asynchronous activation. When we activate, we have to tell the viewer to load a
|
||||||
|
* new html page, which is asynchronous.
|
||||||
|
* @return the future
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected ActivationJob createActivationJob() {
|
||||||
|
|
||||||
|
// start a new page load and then wait for it to finish
|
||||||
|
return (ActivationJob) super.createActivationJob()
|
||||||
|
.thenRun(this::loadMyHelpPage)
|
||||||
|
.thenWait(this::isMyHelpPageShowing, Duration.ofSeconds(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void activate() {
|
||||||
|
//
|
||||||
|
// When we activate, a new page load may get triggered. When that happens the caret
|
||||||
|
// position will get moved by the help viewer. We will put back the last active search
|
||||||
|
// location after the load has finished.
|
||||||
|
//
|
||||||
|
SearchLocation lastActiveLocation = getActiveLocation();
|
||||||
|
FindJob job = startActivation()
|
||||||
|
.thenRunSwing(() -> restoreLocation(lastActiveLocation));
|
||||||
|
|
||||||
|
runActivationJob((ActivationJob) job);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void restoreLocation(SearchLocation lastActiveLocation) {
|
||||||
|
if (lastActiveLocation != null) {
|
||||||
|
setActiveLocation(null);
|
||||||
|
setActiveLocation(lastActiveLocation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispose() {
|
||||||
|
editorPane.removePropertyChangeListener("page", pageLoadListener);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setUrl(URL url) {
|
||||||
|
searchUrl = url;
|
||||||
|
name = getFilename(searchUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private URL getUrl() {
|
||||||
|
return searchUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PageLoadedListener implements PropertyChangeListener {
|
||||||
|
|
||||||
|
private HtmlSearchResults htmlResults;
|
||||||
|
|
||||||
|
PageLoadedListener(HtmlSearchResults htmlResults) {
|
||||||
|
this.htmlResults = htmlResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void propertyChange(PropertyChangeEvent evt) {
|
||||||
|
|
||||||
|
URL newPage = (URL) evt.getNewValue();
|
||||||
|
if (!Objects.equals(newPage, htmlResults.getUrl())) {
|
||||||
|
htmlResults.deactivate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// private class IndexerSearchTask extends Task {
|
// private class IndexerSearchTask extends Task {
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -23,10 +23,14 @@ import javax.swing.*;
|
|||||||
import javax.swing.event.DocumentEvent;
|
import javax.swing.event.DocumentEvent;
|
||||||
import javax.swing.event.DocumentListener;
|
import javax.swing.event.DocumentListener;
|
||||||
|
|
||||||
import docking.ReusableDialogComponentProvider;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import docking.*;
|
||||||
import docking.widgets.button.GRadioButton;
|
import docking.widgets.button.GRadioButton;
|
||||||
import docking.widgets.combobox.GhidraComboBox;
|
import docking.widgets.combobox.GhidraComboBox;
|
||||||
import docking.widgets.label.GLabel;
|
import docking.widgets.label.GLabel;
|
||||||
|
import docking.widgets.search.FindDialogSearcher;
|
||||||
|
import docking.widgets.search.SearchResults;
|
||||||
import utility.function.Callback;
|
import utility.function.Callback;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,8 +41,12 @@ public class FindDialog extends ReusableDialogComponentProvider {
|
|||||||
protected GhidraComboBox<String> comboBox;
|
protected GhidraComboBox<String> comboBox;
|
||||||
|
|
||||||
protected FindDialogSearcher searcher;
|
protected FindDialogSearcher searcher;
|
||||||
|
protected SearchResults searchResults;
|
||||||
|
|
||||||
private JButton nextButton;
|
private JButton nextButton;
|
||||||
private JButton previousButton;
|
private JButton previousButton;
|
||||||
|
private JButton findAllButton;
|
||||||
|
private boolean isFindButtonApiDisabled;
|
||||||
private JRadioButton stringRadioButton;
|
private JRadioButton stringRadioButton;
|
||||||
private JRadioButton regexRadioButton;
|
private JRadioButton regexRadioButton;
|
||||||
|
|
||||||
@@ -49,7 +57,14 @@ public class FindDialog extends ReusableDialogComponentProvider {
|
|||||||
this.searcher = searcher;
|
this.searcher = searcher;
|
||||||
|
|
||||||
addWorkPanel(buildMainPanel());
|
addWorkPanel(buildMainPanel());
|
||||||
buildButtons();
|
buildFindButtons();
|
||||||
|
|
||||||
|
addDismissButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFindAllEnabled(boolean enabled) {
|
||||||
|
isFindButtonApiDisabled = !enabled;
|
||||||
|
findAllButton.setEnabled(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -62,7 +77,7 @@ public class FindDialog extends ReusableDialogComponentProvider {
|
|||||||
this.closedCallback = Callback.dummyIfNull(c);
|
this.closedCallback = Callback.dummyIfNull(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void buildButtons() {
|
protected void buildFindButtons() {
|
||||||
nextButton = new JButton("Next");
|
nextButton = new JButton("Next");
|
||||||
nextButton.setMnemonic('N');
|
nextButton.setMnemonic('N');
|
||||||
nextButton.getAccessibleContext().setAccessibleName("Next");
|
nextButton.getAccessibleContext().setAccessibleName("Next");
|
||||||
@@ -76,7 +91,13 @@ public class FindDialog extends ReusableDialogComponentProvider {
|
|||||||
previousButton.addActionListener(ev -> doSearch(false));
|
previousButton.addActionListener(ev -> doSearch(false));
|
||||||
addButton(previousButton);
|
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() {
|
private JPanel buildMainPanel() {
|
||||||
@@ -137,6 +158,10 @@ public class FindDialog extends ReusableDialogComponentProvider {
|
|||||||
protected void enableButtons(boolean b) {
|
protected void enableButtons(boolean b) {
|
||||||
nextButton.setEnabled(b);
|
nextButton.setEnabled(b);
|
||||||
previousButton.setEnabled(b);
|
previousButton.setEnabled(b);
|
||||||
|
|
||||||
|
if (!isFindButtonApiDisabled) {
|
||||||
|
findAllButton.setEnabled(b);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private JPanel buildFormatPanel() {
|
private JPanel buildFormatPanel() {
|
||||||
@@ -149,13 +174,6 @@ public class FindDialog extends ReusableDialogComponentProvider {
|
|||||||
return formatPanel;
|
return formatPanel;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void dialogClosed() {
|
|
||||||
comboBox.setText("");
|
|
||||||
searcher.clearHighlights();
|
|
||||||
closedCallback.call();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void next() {
|
public void next() {
|
||||||
doSearch(true);
|
doSearch(true);
|
||||||
}
|
}
|
||||||
@@ -168,7 +186,7 @@ public class FindDialog extends ReusableDialogComponentProvider {
|
|||||||
return regexRadioButton.isSelected();
|
return regexRadioButton.isSelected();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doSearch(boolean forward) {
|
protected void doSearch(boolean forward) {
|
||||||
|
|
||||||
if (!nextButton.isEnabled()) {
|
if (!nextButton.isEnabled()) {
|
||||||
return; // don't search while disabled
|
return; // don't search while disabled
|
||||||
@@ -179,14 +197,13 @@ public class FindDialog extends ReusableDialogComponentProvider {
|
|||||||
String searchText = comboBox.getText();
|
String searchText = comboBox.getText();
|
||||||
|
|
||||||
CursorPosition cursorPosition = searcher.getCursorPosition();
|
CursorPosition cursorPosition = searcher.getCursorPosition();
|
||||||
SearchLocation searchLocation =
|
searchResults = searcher.search(searchText, cursorPosition, forward, useRegex);
|
||||||
searcher.search(searchText, cursorPosition, forward, useRegex);
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// First, just search in the current direction.
|
// First, just search in the current direction.
|
||||||
//
|
//
|
||||||
if (searchLocation != null) {
|
if (searchResults != null) {
|
||||||
notifySearchHit(searchLocation);
|
storeSearchText(searchText);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,9 +220,9 @@ public class FindDialog extends ReusableDialogComponentProvider {
|
|||||||
cursorPosition = searcher.getEnd();
|
cursorPosition = searcher.getEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
searchLocation = searcher.search(searchText, cursorPosition, forward, useRegex);
|
searchResults = searcher.search(searchText, cursorPosition, forward, useRegex);
|
||||||
if (searchLocation != null) {
|
if (searchResults != null) {
|
||||||
notifySearchHit(searchLocation);
|
storeSearchText(searchText);
|
||||||
notifyUser(wrapMessage);
|
notifyUser(wrapMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -218,12 +235,6 @@ public class FindDialog extends ReusableDialogComponentProvider {
|
|||||||
notifyUser("Not found");
|
notifyUser("Not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void notifySearchHit(SearchLocation location) {
|
|
||||||
searcher.setCursorPosition(location.getCursorPosition());
|
|
||||||
storeSearchText(location.getSearchText());
|
|
||||||
searcher.highlightSearchResults(location);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifyUser(String message) {
|
private void notifyUser(String message) {
|
||||||
setStatus(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
|
@Override
|
||||||
protected void dialogShown() {
|
protected void dialogShown() {
|
||||||
clearStatusText();
|
clearStatusText();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void dialogClosed() {
|
||||||
|
comboBox.setText("");
|
||||||
|
|
||||||
|
if (searchResults != null) {
|
||||||
|
searchResults.dispose();
|
||||||
|
searchResults = null;
|
||||||
|
}
|
||||||
|
closedCallback.call();
|
||||||
|
}
|
||||||
|
|
||||||
public FindDialogSearcher getSearcher() {
|
public FindDialogSearcher getSearcher() {
|
||||||
return searcher;
|
return searcher;
|
||||||
}
|
}
|
||||||
@@ -286,4 +356,5 @@ public class FindDialog extends ReusableDialogComponentProvider {
|
|||||||
// do this last since removing items may change the selected item
|
// do this last since removing items may change the selected item
|
||||||
model.setSelectedItem(text);
|
model.setSelectedItem(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,342 @@
|
|||||||
|
/* ###
|
||||||
|
* IP: GHIDRA
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package docking.widgets;
|
||||||
|
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.Component;
|
||||||
|
import java.awt.event.MouseAdapter;
|
||||||
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import javax.swing.event.TableModelEvent;
|
||||||
|
import javax.swing.event.TableModelListener;
|
||||||
|
import javax.swing.table.TableModel;
|
||||||
|
|
||||||
|
import docking.ComponentProvider;
|
||||||
|
import docking.Tool;
|
||||||
|
import docking.action.DockingAction;
|
||||||
|
import docking.widgets.search.SearchLocationContext;
|
||||||
|
import docking.widgets.search.SearchResults;
|
||||||
|
import docking.widgets.table.*;
|
||||||
|
import docking.widgets.table.actions.DeleteTableRowAction;
|
||||||
|
import ghidra.docking.settings.Settings;
|
||||||
|
import ghidra.framework.plugintool.ServiceProvider;
|
||||||
|
import ghidra.framework.plugintool.ServiceProviderStub;
|
||||||
|
import ghidra.util.table.column.AbstractGColumnRenderer;
|
||||||
|
import ghidra.util.table.column.GColumnRenderer;
|
||||||
|
|
||||||
|
public class FindDialogResultsProvider extends ComponentProvider
|
||||||
|
implements TableModelListener {
|
||||||
|
|
||||||
|
private static final String OWNER_NAME = "Search";
|
||||||
|
|
||||||
|
private SearchResults searchResults;
|
||||||
|
|
||||||
|
private JPanel componentPanel;
|
||||||
|
private FindResultsModel model;
|
||||||
|
private GTable table;
|
||||||
|
private GTableFilterPanel<SearchLocation> filterPanel;
|
||||||
|
|
||||||
|
private DockingAction removeItemsAction;
|
||||||
|
|
||||||
|
FindDialogResultsProvider(Tool tool, String title, String subTitle,
|
||||||
|
SearchResults searchResults) {
|
||||||
|
|
||||||
|
super(tool, "Find All", OWNER_NAME);
|
||||||
|
this.searchResults = searchResults;
|
||||||
|
|
||||||
|
this.model = new FindResultsModel(searchResults);
|
||||||
|
setTransient();
|
||||||
|
setTitle(title + subTitle);
|
||||||
|
setSubTitle(subTitle);
|
||||||
|
setWindowMenuGroup(title);
|
||||||
|
|
||||||
|
componentPanel = buildMainPanel();
|
||||||
|
updateTitle();
|
||||||
|
|
||||||
|
addToTool();
|
||||||
|
installRemoveItemsAction();
|
||||||
|
|
||||||
|
model.addTableModelListener(this);
|
||||||
|
|
||||||
|
setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JPanel buildMainPanel() {
|
||||||
|
JPanel panel = new JPanel(new BorderLayout());
|
||||||
|
|
||||||
|
table = new GTable(model);
|
||||||
|
table.setHTMLRenderingEnabled(true);
|
||||||
|
filterPanel = new GTableFilterPanel<>(table, model);
|
||||||
|
table.getSelectionModel().addListSelectionListener(e -> {
|
||||||
|
if (e.getValueIsAdjusting()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getTool().contextChanged(FindDialogResultsProvider.this);
|
||||||
|
});
|
||||||
|
|
||||||
|
table.setActionsEnabled(true);
|
||||||
|
|
||||||
|
// add row listener to go to the field for that row when the user arrows
|
||||||
|
ListSelectionModel selectionModel = table.getSelectionModel();
|
||||||
|
selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
|
||||||
|
selectionModel.addListSelectionListener(lse -> {
|
||||||
|
if (lse.getValueIsAdjusting()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearchLocationFromRow();
|
||||||
|
});
|
||||||
|
|
||||||
|
// this listener works around the case where the user clicks and already selected row
|
||||||
|
table.addMouseListener(new MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mouseClicked(MouseEvent e) {
|
||||||
|
if (e.getClickCount() < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearchLocationFromRow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.add(new JScrollPane(table), BorderLayout.CENTER);
|
||||||
|
panel.add(filterPanel, BorderLayout.SOUTH);
|
||||||
|
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setSearchLocationFromRow() {
|
||||||
|
int row = table.getSelectedRow();
|
||||||
|
if (row == -1) {
|
||||||
|
searchResults.setActiveLocation(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchLocation location = model.getRowObject(row);
|
||||||
|
searchResults.setActiveLocation(location);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void installRemoveItemsAction() {
|
||||||
|
if (removeItemsAction != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeItemsAction = new DeleteTableRowAction(table, OWNER_NAME) {
|
||||||
|
@Override
|
||||||
|
protected void removeSelectedItems() {
|
||||||
|
|
||||||
|
int[] rows = table.getSelectedRows();
|
||||||
|
List<Object> itemsToRemove = new ArrayList<>();
|
||||||
|
for (int row : rows) {
|
||||||
|
itemsToRemove.add(model.getRowObject(row));
|
||||||
|
}
|
||||||
|
|
||||||
|
removeRowObjects(model, itemsToRemove);
|
||||||
|
|
||||||
|
// put some selection back
|
||||||
|
int restoreRow = rows[0];
|
||||||
|
selectRow(model, restoreRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void removeRowObjects(TableModel tm, List<Object> itemsToRemove) {
|
||||||
|
model.remove(itemsToRemove);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getTool().addLocalAction(this, removeItemsAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateSubTitle() {
|
||||||
|
StringBuilder buffer = new StringBuilder();
|
||||||
|
String filteredText = "";
|
||||||
|
if (filterPanel.isFiltered()) {
|
||||||
|
filteredText = " of " + filterPanel.getUnfilteredRowCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
int n = model.getRowCount();
|
||||||
|
if (n == 1) {
|
||||||
|
buffer.append(" (1 entry").append(filteredText).append(")");
|
||||||
|
}
|
||||||
|
else if (n > 1) {
|
||||||
|
buffer.append(" (").append(n).append(" entries").append(filteredText).append(")");
|
||||||
|
}
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void closeComponent() {
|
||||||
|
searchResults.dispose();
|
||||||
|
|
||||||
|
super.closeComponent();
|
||||||
|
|
||||||
|
filterPanel.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JComponent getComponent() {
|
||||||
|
return componentPanel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void componentActivated() {
|
||||||
|
searchResults.activate();
|
||||||
|
|
||||||
|
setSearchLocationFromRow();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void componentDeactived() {
|
||||||
|
// We don't want this, as the user may wish to click around in the text pane and keep the
|
||||||
|
// highlights, which this call would break.
|
||||||
|
// searchResults.deactivate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void tableChanged(TableModelEvent ev) {
|
||||||
|
updateTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateTitle() {
|
||||||
|
setSubTitle(generateSubTitle());
|
||||||
|
}
|
||||||
|
|
||||||
|
public GTable getTable() {
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
// for testing
|
||||||
|
public List<SearchLocation> getResults() {
|
||||||
|
return new ArrayList<>(model.getModelData());
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FindResultsModel extends GDynamicColumnTableModel<SearchLocation, Object> {
|
||||||
|
|
||||||
|
private List<SearchLocation> data;
|
||||||
|
|
||||||
|
FindResultsModel(SearchResults results) {
|
||||||
|
super(new ServiceProviderStub());
|
||||||
|
this.data = results.getLocations();
|
||||||
|
}
|
||||||
|
|
||||||
|
void remove(List<Object> itemsToRemove) {
|
||||||
|
for (Object object : itemsToRemove) {
|
||||||
|
data.remove(object);
|
||||||
|
}
|
||||||
|
fireTableDataChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SearchLocation> getModelData() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected TableColumnDescriptor<SearchLocation> createTableColumnDescriptor() {
|
||||||
|
|
||||||
|
TableColumnDescriptor<SearchLocation> descriptor =
|
||||||
|
new TableColumnDescriptor<>();
|
||||||
|
descriptor.addVisibleColumn(new LineNumberColumn(), 1, true);
|
||||||
|
descriptor.addVisibleColumn(new ContextColumn());
|
||||||
|
return descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "Find All Results";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getDataSource() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LineNumberColumn
|
||||||
|
extends AbstractDynamicTableColumnStub<SearchLocation, Integer> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer getValue(SearchLocation rowObject, Settings settings,
|
||||||
|
ServiceProvider sp) throws IllegalArgumentException {
|
||||||
|
return rowObject.getLineNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getColumnName() {
|
||||||
|
return "Line";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getColumnPreferredWidth() {
|
||||||
|
return 75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ContextColumn extends
|
||||||
|
AbstractDynamicTableColumnStub<SearchLocation, SearchLocationContext> {
|
||||||
|
|
||||||
|
private GColumnRenderer<SearchLocationContext> renderer = new ContextCellRenderer();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SearchLocationContext getValue(SearchLocation rowObject,
|
||||||
|
Settings settings,
|
||||||
|
ServiceProvider sp) throws IllegalArgumentException {
|
||||||
|
|
||||||
|
SearchLocationContext context = rowObject.getContext();
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getColumnName() {
|
||||||
|
return "Context";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GColumnRenderer<SearchLocationContext> getColumnRenderer() {
|
||||||
|
return renderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ContextCellRenderer
|
||||||
|
extends AbstractGColumnRenderer<SearchLocationContext> {
|
||||||
|
|
||||||
|
{
|
||||||
|
// the context uses html
|
||||||
|
setHTMLRenderingEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Component getTableCellRendererComponent(GTableCellRenderingData cellData) {
|
||||||
|
|
||||||
|
// initialize
|
||||||
|
super.getTableCellRendererComponent(cellData);
|
||||||
|
|
||||||
|
SearchLocation match = (SearchLocation) cellData.getRowObject();
|
||||||
|
SearchLocationContext context = match.getContext();
|
||||||
|
String text = context.getBoldMatchingText();
|
||||||
|
setText(text);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFilterString(SearchLocationContext context, Settings settings) {
|
||||||
|
return context.getPlainText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,9 +4,9 @@
|
|||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
@@ -15,32 +15,47 @@
|
|||||||
*/
|
*/
|
||||||
package docking.widgets;
|
package docking.widgets;
|
||||||
|
|
||||||
|
import docking.widgets.search.SearchLocationContext;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An object that describes a search result.
|
* An object that describes a search result.
|
||||||
*/
|
*/
|
||||||
public class SearchLocation {
|
public class SearchLocation {
|
||||||
private final int startIndexInclusive;
|
private final int startIndexInclusive;
|
||||||
private final int endIndexInclusive;
|
private final int endIndexInclusive;
|
||||||
private final String searchText;
|
private final String text;
|
||||||
private final boolean forwardDirection;
|
private SearchLocationContext context;
|
||||||
|
private int lineNumber;
|
||||||
|
|
||||||
public SearchLocation(int startIndexInclusive, int endIndexInclusive, String searchText,
|
public SearchLocation(int startIndexInclusive, int endIndexInclusive, String text) {
|
||||||
boolean forwardDirection) {
|
|
||||||
|
|
||||||
this.startIndexInclusive = startIndexInclusive;
|
this.startIndexInclusive = startIndexInclusive;
|
||||||
this.endIndexInclusive = endIndexInclusive;
|
this.endIndexInclusive = endIndexInclusive;
|
||||||
this.searchText = searchText;
|
this.text = text;
|
||||||
this.forwardDirection = forwardDirection;
|
}
|
||||||
|
|
||||||
|
public SearchLocation(int startIndexInclusive, int endIndexInclusive, String text,
|
||||||
|
int lineNumber, SearchLocationContext context) {
|
||||||
|
|
||||||
|
this.startIndexInclusive = startIndexInclusive;
|
||||||
|
this.endIndexInclusive = endIndexInclusive;
|
||||||
|
this.text = text;
|
||||||
|
this.context = context;
|
||||||
|
this.lineNumber = lineNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SearchLocationContext getContext() {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLineNumber() {
|
||||||
|
return lineNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CursorPosition getCursorPosition() {
|
public CursorPosition getCursorPosition() {
|
||||||
return new CursorPosition(startIndexInclusive);
|
return new CursorPosition(startIndexInclusive);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSearchText() {
|
|
||||||
return searchText;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getEndIndexInclusive() {
|
public int getEndIndexInclusive() {
|
||||||
return endIndexInclusive;
|
return endIndexInclusive;
|
||||||
}
|
}
|
||||||
@@ -49,20 +64,21 @@ public class SearchLocation {
|
|||||||
return startIndexInclusive;
|
return startIndexInclusive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean contains(int pos) {
|
||||||
|
return startIndexInclusive <= pos && endIndexInclusive >= pos;
|
||||||
|
}
|
||||||
|
|
||||||
public int getMatchLength() {
|
public int getMatchLength() {
|
||||||
return endIndexInclusive - startIndexInclusive + 1;
|
return endIndexInclusive - startIndexInclusive + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isForwardDirection() {
|
|
||||||
return forwardDirection;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return searchText + "[" + fieldsToString() + "]";
|
return text + "[" + fieldsToString() + "]";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String fieldsToString() {
|
protected String fieldsToString() {
|
||||||
return startIndexInclusive + ", end=" + endIndexInclusive;
|
return "line=%s, start=%s, end=%s".formatted(lineNumber, startIndexInclusive,
|
||||||
|
endIndexInclusive);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,539 +0,0 @@
|
|||||||
/* ###
|
|
||||||
* IP: GHIDRA
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package docking.widgets;
|
|
||||||
|
|
||||||
import java.awt.*;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Map.Entry;
|
|
||||||
import java.util.TreeMap;
|
|
||||||
import java.util.regex.*;
|
|
||||||
|
|
||||||
import javax.swing.JEditorPane;
|
|
||||||
import javax.swing.event.*;
|
|
||||||
import javax.swing.text.*;
|
|
||||||
|
|
||||||
import generic.theme.GColor;
|
|
||||||
import ghidra.util.Msg;
|
|
||||||
import ghidra.util.UserSearchUtils;
|
|
||||||
import ghidra.util.exception.CancelledException;
|
|
||||||
import ghidra.util.task.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A class to find text matches in the given {@link TextComponent}. This class will search for all
|
|
||||||
* matches and cache the results for future requests when the user presses Next or Previous. All
|
|
||||||
* matches will be highlighted in the text component. The match containing the cursor will be a
|
|
||||||
* different highlight color than the others. When the find dialog is closed, all highlights are
|
|
||||||
* removed.
|
|
||||||
*/
|
|
||||||
public class TextComponentSearcher implements FindDialogSearcher {
|
|
||||||
|
|
||||||
private Color highlightColor = new GColor("color.bg.find.highlight");
|
|
||||||
private Color activeHighlightColor = new GColor("color.bg.find.highlight.active");
|
|
||||||
|
|
||||||
private JEditorPane editorPane;
|
|
||||||
private DocumentListener documentListener = new DocumentChangeListener();
|
|
||||||
|
|
||||||
private CaretListener caretListener = new CaretChangeListener();
|
|
||||||
private SwingUpdateManager caretUpdater = new SwingUpdateManager(() -> updateActiveHighlight());
|
|
||||||
private volatile boolean isUpdatingCaretInternally;
|
|
||||||
|
|
||||||
private SearchResults searchResults;
|
|
||||||
|
|
||||||
public TextComponentSearcher(JEditorPane editorPane) {
|
|
||||||
this.editorPane = editorPane;
|
|
||||||
|
|
||||||
if (editorPane == null) {
|
|
||||||
return; // some clients initialize without an editor pane
|
|
||||||
}
|
|
||||||
|
|
||||||
Document document = editorPane.getDocument();
|
|
||||||
document.addDocumentListener(documentListener);
|
|
||||||
|
|
||||||
editorPane.addCaretListener(caretListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setEditorPane(JEditorPane editorPane) {
|
|
||||||
if (this.editorPane != editorPane) {
|
|
||||||
Document document = editorPane.getDocument();
|
|
||||||
document.removeDocumentListener(documentListener);
|
|
||||||
markResultsStale();
|
|
||||||
}
|
|
||||||
this.editorPane = editorPane;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JEditorPane getEditorPane() {
|
|
||||||
return editorPane;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void dispose() {
|
|
||||||
caretUpdater.dispose();
|
|
||||||
|
|
||||||
if (editorPane != null) {
|
|
||||||
Document document = editorPane.getDocument();
|
|
||||||
document.removeDocumentListener(documentListener);
|
|
||||||
|
|
||||||
clearHighlights();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clearHighlights() {
|
|
||||||
if (searchResults != null) {
|
|
||||||
searchResults.removeHighlights();
|
|
||||||
searchResults = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasSearchResults() {
|
|
||||||
return searchResults != null && !searchResults.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isStale() {
|
|
||||||
return searchResults != null && searchResults.isStale();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void markResultsStale() {
|
|
||||||
if (searchResults != null) {
|
|
||||||
searchResults.setStale();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateActiveHighlight() {
|
|
||||||
if (searchResults == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int pos = editorPane.getCaretPosition();
|
|
||||||
searchResults.updateActiveMatch(pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setCaretPositionInternally(int pos) {
|
|
||||||
isUpdatingCaretInternally = true;
|
|
||||||
try {
|
|
||||||
editorPane.setCaretPosition(pos);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
isUpdatingCaretInternally = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CursorPosition getCursorPosition() {
|
|
||||||
int pos = editorPane.getCaretPosition();
|
|
||||||
return new CursorPosition(pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setCursorPosition(CursorPosition position) {
|
|
||||||
int pos = position.getPosition();
|
|
||||||
editorPane.setCaretPosition(pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CursorPosition getStart() {
|
|
||||||
return new CursorPosition(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CursorPosition getEnd() {
|
|
||||||
int length = editorPane.getDocument().getLength();
|
|
||||||
return new CursorPosition(length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void highlightSearchResults(SearchLocation location) {
|
|
||||||
|
|
||||||
if (location == null) {
|
|
||||||
clearHighlights();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
TextComponentSearchLocation textLocation = (TextComponentSearchLocation) location;
|
|
||||||
FindMatch match = textLocation.getMatch();
|
|
||||||
searchResults.setActiveMatch(match);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SearchLocation search(String text, CursorPosition cursorPosition,
|
|
||||||
boolean searchForward, boolean useRegex) {
|
|
||||||
|
|
||||||
updateSearchResults(text, useRegex);
|
|
||||||
|
|
||||||
int pos = cursorPosition.getPosition();
|
|
||||||
int searchStart = getSearchStart(pos, searchForward);
|
|
||||||
|
|
||||||
FindMatch match = searchResults.getNextMatch(searchStart, searchForward);
|
|
||||||
if (match == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new TextComponentSearchLocation(match.getStart(), match.getEnd(), text,
|
|
||||||
searchForward, match);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateSearchResults(String text, boolean useRegex) {
|
|
||||||
if (searchResults != null) {
|
|
||||||
if (!searchResults.isInvalid(text)) {
|
|
||||||
return; // the current results are still valid
|
|
||||||
}
|
|
||||||
|
|
||||||
searchResults.removeHighlights();
|
|
||||||
}
|
|
||||||
|
|
||||||
SearchTask searchTask = new SearchTask(text, useRegex);
|
|
||||||
TaskLauncher.launch(searchTask);
|
|
||||||
searchResults = searchTask.getSearchResults();
|
|
||||||
searchResults.applyHighlights();
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getSearchStart(int startPosition, boolean isForward) {
|
|
||||||
|
|
||||||
FindMatch activeMatch = searchResults.getActiveMatch();
|
|
||||||
if (activeMatch == null) {
|
|
||||||
return startPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
int lastMatchStart = activeMatch.getStart();
|
|
||||||
if (startPosition != lastMatchStart) {
|
|
||||||
return startPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always prefer the caret position, unless it aligns with the previous match. By
|
|
||||||
// moving it forward one we will continue our search, as opposed to always matching
|
|
||||||
// the same hit.
|
|
||||||
if (isForward) {
|
|
||||||
return startPosition + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// backwards
|
|
||||||
if (startPosition == 0) {
|
|
||||||
return editorPane.getText().length();
|
|
||||||
}
|
|
||||||
return startPosition - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
//=================================================================================================
|
|
||||||
// Inner Classes
|
|
||||||
//=================================================================================================
|
|
||||||
|
|
||||||
private class SearchResults {
|
|
||||||
|
|
||||||
private TreeMap<Integer, FindMatch> matchesByPosition;
|
|
||||||
private FindMatch activeMatch;
|
|
||||||
private boolean isStale;
|
|
||||||
private String searchText;
|
|
||||||
|
|
||||||
SearchResults(String searchText, TreeMap<Integer, FindMatch> matchesByPosition) {
|
|
||||||
this.searchText = searchText;
|
|
||||||
this.matchesByPosition = matchesByPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isStale() {
|
|
||||||
return isStale;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateActiveMatch(int pos) {
|
|
||||||
if (activeMatch != null) {
|
|
||||||
activeMatch.setActive(false);
|
|
||||||
activeMatch = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isStale) {
|
|
||||||
// not way to easily change highlights for the caret position while we are stale,
|
|
||||||
// since the matches no longer match the document positions
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (FindMatch match : matchesByPosition.values()) {
|
|
||||||
boolean isActive = false;
|
|
||||||
if (match.contains(pos)) {
|
|
||||||
activeMatch = match;
|
|
||||||
isActive = true;
|
|
||||||
}
|
|
||||||
match.setActive(isActive);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FindMatch getActiveMatch() {
|
|
||||||
return activeMatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
FindMatch getNextMatch(int searchStart, boolean searchForward) {
|
|
||||||
|
|
||||||
Entry<Integer, FindMatch> entry;
|
|
||||||
if (searchForward) {
|
|
||||||
entry = matchesByPosition.ceilingEntry(searchStart);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
entry = matchesByPosition.floorEntry(searchStart);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry == null) {
|
|
||||||
return null; // no more matches in the current direction
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry.getValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isEmpty() {
|
|
||||||
return matchesByPosition.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setStale() {
|
|
||||||
isStale = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isInvalid(String otherSearchText) {
|
|
||||||
if (isStale) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return !searchText.equals(otherSearchText);
|
|
||||||
}
|
|
||||||
|
|
||||||
void setActiveMatch(FindMatch match) {
|
|
||||||
if (activeMatch != null) {
|
|
||||||
activeMatch.setActive(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
activeMatch = match;
|
|
||||||
activeMatch.activate();
|
|
||||||
}
|
|
||||||
|
|
||||||
void applyHighlights() {
|
|
||||||
Collection<FindMatch> matches = matchesByPosition.values();
|
|
||||||
for (FindMatch match : matches) {
|
|
||||||
match.applyHighlight();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeHighlights() {
|
|
||||||
|
|
||||||
activeMatch = null;
|
|
||||||
|
|
||||||
JEditorPane editor = editorPane;
|
|
||||||
Highlighter highlighter = editor.getHighlighter();
|
|
||||||
if (highlighter != null) {
|
|
||||||
highlighter.removeAllHighlights();
|
|
||||||
}
|
|
||||||
|
|
||||||
matchesByPosition.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TextComponentSearchLocation extends SearchLocation {
|
|
||||||
|
|
||||||
private FindMatch match;
|
|
||||||
|
|
||||||
public TextComponentSearchLocation(int start, int end,
|
|
||||||
String searchText, boolean forwardDirection, FindMatch match) {
|
|
||||||
super(start, end, searchText, forwardDirection);
|
|
||||||
this.match = match;
|
|
||||||
}
|
|
||||||
|
|
||||||
FindMatch getMatch() {
|
|
||||||
return match;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SearchTask extends Task {
|
|
||||||
|
|
||||||
private String searchText;
|
|
||||||
private TreeMap<Integer, FindMatch> searchHits = new TreeMap<>();
|
|
||||||
private boolean useRegex;
|
|
||||||
|
|
||||||
SearchTask(String searchText, boolean useRegex) {
|
|
||||||
super("Help Search Task", true, false, true, true);
|
|
||||||
this.searchText = searchText;
|
|
||||||
this.useRegex = useRegex;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run(TaskMonitor monitor) throws CancelledException {
|
|
||||||
|
|
||||||
String screenText;
|
|
||||||
try {
|
|
||||||
Document document = editorPane.getDocument();
|
|
||||||
screenText = document.getText(0, document.getLength());
|
|
||||||
}
|
|
||||||
catch (BadLocationException e) {
|
|
||||||
Msg.error(this, "Unable to get text for user find operation", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Pattern pattern = createSearchPattern(searchText, useRegex);
|
|
||||||
Matcher matcher = pattern.matcher(screenText);
|
|
||||||
while (matcher.find()) {
|
|
||||||
monitor.checkCancelled();
|
|
||||||
int start = matcher.start();
|
|
||||||
int end = matcher.end();
|
|
||||||
FindMatch match = new FindMatch(searchText, start, end);
|
|
||||||
searchHits.put(start, match);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private Pattern createSearchPattern(String searchString, boolean isRegex) {
|
|
||||||
|
|
||||||
int options = Pattern.CASE_INSENSITIVE | Pattern.DOTALL;
|
|
||||||
if (isRegex) {
|
|
||||||
try {
|
|
||||||
return Pattern.compile(searchString, options);
|
|
||||||
}
|
|
||||||
catch (PatternSyntaxException e) {
|
|
||||||
Msg.showError(this, editorPane, "Regular Expression Syntax Error",
|
|
||||||
e.getMessage());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return UserSearchUtils.createPattern(searchString, false, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
SearchResults getSearchResults() {
|
|
||||||
return new SearchResults(searchText, searchHits);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class FindMatch {
|
|
||||||
|
|
||||||
private String text;
|
|
||||||
private int start;
|
|
||||||
private int end;
|
|
||||||
private boolean isActive;
|
|
||||||
|
|
||||||
// this tag is a way to remove an installed highlight
|
|
||||||
private Object lastHighlightTag;
|
|
||||||
|
|
||||||
FindMatch(String text, int start, int end) {
|
|
||||||
this.start = start;
|
|
||||||
this.end = end;
|
|
||||||
this.text = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean contains(int pos) {
|
|
||||||
// exclusive of end so the cursor behind the match does is not in the highlight
|
|
||||||
return start <= pos && pos < end;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Calls setActive() and moves the caret position */
|
|
||||||
void activate() {
|
|
||||||
setActive(true);
|
|
||||||
setCaretPositionInternally(start);
|
|
||||||
scrollToVisible();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes this match active and updates the highlight color
|
|
||||||
* @param b true for active
|
|
||||||
*/
|
|
||||||
void setActive(boolean b) {
|
|
||||||
isActive = b;
|
|
||||||
applyHighlight();
|
|
||||||
}
|
|
||||||
|
|
||||||
int getStart() {
|
|
||||||
return start;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getEnd() {
|
|
||||||
return end;
|
|
||||||
}
|
|
||||||
|
|
||||||
void scrollToVisible() {
|
|
||||||
|
|
||||||
try {
|
|
||||||
Rectangle startR = editorPane.modelToView2D(start).getBounds();
|
|
||||||
Rectangle endR = editorPane.modelToView2D(end).getBounds();
|
|
||||||
endR.width += 20; // a little extra space so the view is not right at the text end
|
|
||||||
Rectangle union = startR.union(endR);
|
|
||||||
editorPane.scrollRectToVisible(union);
|
|
||||||
}
|
|
||||||
catch (BadLocationException e) {
|
|
||||||
Msg.debug(this, "Exception scrolling to text", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "[" + start + ',' + end + "] " + text;
|
|
||||||
}
|
|
||||||
|
|
||||||
void applyHighlight() {
|
|
||||||
Highlighter highlighter = editorPane.getHighlighter();
|
|
||||||
if (highlighter == null) {
|
|
||||||
highlighter = new DefaultHighlighter();
|
|
||||||
editorPane.setHighlighter(highlighter);
|
|
||||||
}
|
|
||||||
|
|
||||||
Highlighter.HighlightPainter painter =
|
|
||||||
new DefaultHighlighter.DefaultHighlightPainter(
|
|
||||||
isActive ? activeHighlightColor : highlightColor);
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
if (lastHighlightTag != null) {
|
|
||||||
highlighter.removeHighlight(lastHighlightTag);
|
|
||||||
}
|
|
||||||
|
|
||||||
lastHighlightTag = highlighter.addHighlight(start, end, painter);
|
|
||||||
}
|
|
||||||
catch (BadLocationException e) {
|
|
||||||
Msg.debug(this, "Exception adding highlight", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DocumentChangeListener implements DocumentListener {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void insertUpdate(DocumentEvent e) {
|
|
||||||
// this allows the previous search results to stay visible until a new find is requested
|
|
||||||
markResultsStale();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void removeUpdate(DocumentEvent e) {
|
|
||||||
markResultsStale();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void changedUpdate(DocumentEvent e) {
|
|
||||||
// ignore attribute changes since they don't affect the text content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class CaretChangeListener implements CaretListener {
|
|
||||||
|
|
||||||
private int lastPos = -1;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void caretUpdate(CaretEvent e) {
|
|
||||||
int pos = e.getDot();
|
|
||||||
if (isUpdatingCaretInternally) {
|
|
||||||
lastPos = pos;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pos == lastPos) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lastPos = pos;
|
|
||||||
caretUpdater.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,11 +13,10 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package docking.widgets;
|
package docking.widgets.search;
|
||||||
|
|
||||||
import java.util.List;
|
import docking.widgets.CursorPosition;
|
||||||
|
import docking.widgets.FindDialog;
|
||||||
import javax.help.UnsupportedOperationException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple interface for the {@link FindDialog} so that it can work for different search clients.
|
* A simple interface for the {@link FindDialog} so that it can work for different search clients.
|
||||||
@@ -25,6 +24,10 @@ import javax.help.UnsupportedOperationException;
|
|||||||
* The {@link CursorPosition} object used by this interface is one that implementations can extend
|
* The {@link CursorPosition} object used by this interface is one that implementations can extend
|
||||||
* to add extra context to use when searching. The implementation is responsible for creating the
|
* to add extra context to use when searching. The implementation is responsible for creating the
|
||||||
* locations and these locations will later be handed back to the searcher.
|
* locations and these locations will later be handed back to the searcher.
|
||||||
|
* <p>
|
||||||
|
* The {@link FindDialog} should use a single searcher for the life of the dialog. This allows all
|
||||||
|
* search results generated by that dialog to share the same worker queue for running background
|
||||||
|
* operations related to managing search results.
|
||||||
*/
|
*/
|
||||||
public interface FindDialogSearcher {
|
public interface FindDialogSearcher {
|
||||||
|
|
||||||
@@ -34,12 +37,6 @@ public interface FindDialogSearcher {
|
|||||||
*/
|
*/
|
||||||
public CursorPosition getCursorPosition();
|
public CursorPosition getCursorPosition();
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the cursor position after a successful search.
|
|
||||||
* @param position the cursor position.
|
|
||||||
*/
|
|
||||||
public void setCursorPosition(CursorPosition position);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the start cursor position. This is used when a search is wrapped to start at the
|
* Returns the start cursor position. This is used when a search is wrapped to start at the
|
||||||
* beginning of the search range.
|
* beginning of the search range.
|
||||||
@@ -54,45 +51,28 @@ public interface FindDialogSearcher {
|
|||||||
*/
|
*/
|
||||||
public CursorPosition getEnd();
|
public CursorPosition getEnd();
|
||||||
|
|
||||||
/**
|
|
||||||
* Called to signal the implementor should highlight the given search location.
|
|
||||||
* @param location the search result location.
|
|
||||||
*/
|
|
||||||
public void highlightSearchResults(SearchLocation location);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears any active highlights.
|
|
||||||
*/
|
|
||||||
public void clearHighlights();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform a search for the next item in the given direction starting at the given cursor
|
* Perform a search for the next item in the given direction starting at the given cursor
|
||||||
* position.
|
* position.
|
||||||
* @param text the search text.
|
* @param text the search text.
|
||||||
* @param cursorPosition the current cursor position.
|
* @param cursorPosition the current cursor position.
|
||||||
* @param searchForward true if searching forward.
|
* @param searchForward true if searching forward.
|
||||||
* @param useRegex true if the search text is a regular expression; false if the texts is
|
* @param useRegex true if the search text is a regular expression; false if the text is literal.
|
||||||
* literal text.
|
|
||||||
* @return the search result or null if no match was found.
|
* @return the search result or null if no match was found.
|
||||||
*/
|
*/
|
||||||
public SearchLocation search(String text, CursorPosition cursorPosition, boolean searchForward,
|
public SearchResults search(String text, CursorPosition cursorPosition, boolean searchForward,
|
||||||
boolean useRegex);
|
boolean useRegex);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search for all matches.
|
* Search for all matches.
|
||||||
* @param text the search text.
|
* @param text the search text.
|
||||||
* @param useRegex true if the search text is a regular expression; false if the texts is
|
* @param useRegex true if the search text is a regular expression; false if the text is literal.
|
||||||
* literal text.
|
|
||||||
* @return all search results or an empty list.
|
* @return all search results or an empty list.
|
||||||
*/
|
*/
|
||||||
public default List<SearchLocation> searchAll(String text, boolean useRegex) {
|
public SearchResults searchAll(String text, boolean useRegex);
|
||||||
throw new UnsupportedOperationException("Search All is not defined for this searcher");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disposes this searcher. This does nothing by default.
|
* Disposes this searcher.
|
||||||
*/
|
*/
|
||||||
public default void dispose() {
|
public void dispose();
|
||||||
// stub
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -13,25 +13,26 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package ghidra.app.plugin.core.navigation.locationreferences;
|
package docking.widgets.search;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
import docking.widgets.SearchLocation;
|
||||||
import generic.json.Json;
|
import generic.json.Json;
|
||||||
import ghidra.util.HTMLUtilities;
|
import ghidra.util.HTMLUtilities;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class to hold context representation for {@link LocationReference}s.
|
* A class to hold context representation for {@link SearchLocation}s.
|
||||||
*
|
*
|
||||||
* @see LocationReferenceContextBuilder
|
* @see SearchLocationContextBuilder
|
||||||
*/
|
*/
|
||||||
public class LocationReferenceContext {
|
public class SearchLocationContext {
|
||||||
|
|
||||||
private static final String EMBOLDEN_START =
|
private static final String EMBOLDEN_START =
|
||||||
"<span style=\"background-color: #a3e4d7; color: black;\"><b><font size=4>";
|
"<span style=\"background-color: #a3e4d7; color: black;\"><b><font size=4>";
|
||||||
private static final String EMBOLDEN_END = "</font></b></span>";
|
private static final String EMBOLDEN_END = "</font></b></span>";
|
||||||
|
|
||||||
public static final LocationReferenceContext EMPTY_CONTEXT = new LocationReferenceContext();
|
public static final SearchLocationContext EMPTY_CONTEXT = new SearchLocationContext();
|
||||||
|
|
||||||
private final List<Part> parts;
|
private final List<Part> parts;
|
||||||
|
|
||||||
@@ -42,8 +43,8 @@ public class LocationReferenceContext {
|
|||||||
* @param text the text
|
* @param text the text
|
||||||
* @return the context
|
* @return the context
|
||||||
*/
|
*/
|
||||||
public static LocationReferenceContext get(String text) {
|
public static SearchLocationContext get(String text) {
|
||||||
return text == null ? EMPTY_CONTEXT : new LocationReferenceContext(text);
|
return text == null ? EMPTY_CONTEXT : new SearchLocationContext(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,14 +52,14 @@ public class LocationReferenceContext {
|
|||||||
* @param context the context to verify is not null
|
* @param context the context to verify is not null
|
||||||
* @return the given context or the {@link #EMPTY_CONTEXT} if the given context is null
|
* @return the given context or the {@link #EMPTY_CONTEXT} if the given context is null
|
||||||
*/
|
*/
|
||||||
public static LocationReferenceContext get(LocationReferenceContext context) {
|
public static SearchLocationContext get(SearchLocationContext context) {
|
||||||
return context == null ? EMPTY_CONTEXT : context;
|
return context == null ? EMPTY_CONTEXT : context;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an empty context object
|
* Creates an empty context object
|
||||||
*/
|
*/
|
||||||
private LocationReferenceContext() {
|
private SearchLocationContext() {
|
||||||
this.parts = List.of(new BasicPart(""));
|
this.parts = List.of(new BasicPart(""));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ public class LocationReferenceContext {
|
|||||||
* Creates a context with the raw and decorated context being the same.
|
* Creates a context with the raw and decorated context being the same.
|
||||||
* @param context the context; cannot be null
|
* @param context the context; cannot be null
|
||||||
*/
|
*/
|
||||||
private LocationReferenceContext(String context) {
|
private SearchLocationContext(String context) {
|
||||||
Objects.requireNonNull(context);
|
Objects.requireNonNull(context);
|
||||||
this.parts = List.of(new BasicPart(context));
|
this.parts = List.of(new BasicPart(context));
|
||||||
}
|
}
|
||||||
@@ -74,9 +75,9 @@ public class LocationReferenceContext {
|
|||||||
/**
|
/**
|
||||||
* Constructor used to create this context by providing the given text parts
|
* Constructor used to create this context by providing the given text parts
|
||||||
* @param parts the parts
|
* @param parts the parts
|
||||||
* @see LocationReferenceContextBuilder
|
* @see SearchLocationContextBuilder
|
||||||
*/
|
*/
|
||||||
LocationReferenceContext(List<Part> parts) {
|
SearchLocationContext(List<Part> parts) {
|
||||||
this.parts = parts;
|
this.parts = parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +121,7 @@ public class LocationReferenceContext {
|
|||||||
/**
|
/**
|
||||||
* Returns any sub-strings of this context's overall text that match client-defined input
|
* Returns any sub-strings of this context's overall text that match client-defined input
|
||||||
*
|
*
|
||||||
* See the {@link LocationReferenceContextBuilder} for how to define matching text pieces
|
* See the {@link SearchLocationContextBuilder} for how to define matching text pieces
|
||||||
* @return the matching strings
|
* @return the matching strings
|
||||||
*/
|
*/
|
||||||
public List<String> getMatches() {
|
public List<String> getMatches() {
|
||||||
@@ -13,20 +13,20 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package ghidra.app.plugin.core.navigation.locationreferences;
|
package docking.widgets.search;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import docking.widgets.search.SearchLocationContext.*;
|
||||||
import generic.json.Json;
|
import generic.json.Json;
|
||||||
import ghidra.app.plugin.core.navigation.locationreferences.LocationReferenceContext.*;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A builder for {@link LocationReferenceContext} objects. Use {@link #append(String)} for normal
|
* A builder for {@link SearchLocationContext} objects. Use {@link #append(String)} for normal
|
||||||
* text pieces. Use {@link #appendMatch(String)} for text that is meant to be rendered specially
|
* text pieces. Use {@link #appendMatch(String)} for text that is meant to be rendered specially
|
||||||
* by the context class.
|
* by the context class.
|
||||||
*/
|
*/
|
||||||
public class LocationReferenceContextBuilder {
|
public class SearchLocationContextBuilder {
|
||||||
|
|
||||||
private List<Part> parts = new ArrayList<>();
|
private List<Part> parts = new ArrayList<>();
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ public class LocationReferenceContextBuilder {
|
|||||||
* @param text the text
|
* @param text the text
|
||||||
* @return this builder
|
* @return this builder
|
||||||
*/
|
*/
|
||||||
public LocationReferenceContextBuilder append(String text) {
|
public SearchLocationContextBuilder append(String text) {
|
||||||
if (text == null) {
|
if (text == null) {
|
||||||
text = "";
|
text = "";
|
||||||
}
|
}
|
||||||
@@ -45,12 +45,12 @@ public class LocationReferenceContextBuilder {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Appends the given text to this builder. This text represents a client-defined 'match' that
|
* Appends the given text to this builder. This text represents a client-defined 'match' that
|
||||||
* will be rendered with markup when {@link LocationReferenceContext#getBoldMatchingText()} is
|
* will be rendered with markup when {@link SearchLocationContext#getBoldMatchingText()} is
|
||||||
* called.
|
* called.
|
||||||
* @param text the text
|
* @param text the text
|
||||||
* @return this builder
|
* @return this builder
|
||||||
*/
|
*/
|
||||||
public LocationReferenceContextBuilder appendMatch(String text) {
|
public SearchLocationContextBuilder appendMatch(String text) {
|
||||||
if (text == null) {
|
if (text == null) {
|
||||||
throw new NullPointerException("Match text cannot be null");
|
throw new NullPointerException("Match text cannot be null");
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,7 @@ public class LocationReferenceContextBuilder {
|
|||||||
* Adds a newline character to the previously added text.
|
* Adds a newline character to the previously added text.
|
||||||
* @return this builder
|
* @return this builder
|
||||||
*/
|
*/
|
||||||
public LocationReferenceContextBuilder newline() {
|
public SearchLocationContextBuilder newline() {
|
||||||
if (parts.isEmpty()) {
|
if (parts.isEmpty()) {
|
||||||
throw new IllegalStateException("Cannot add a newline without first appending text");
|
throw new IllegalStateException("Cannot add a newline without first appending text");
|
||||||
}
|
}
|
||||||
@@ -72,12 +72,12 @@ public class LocationReferenceContextBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a {@link LocationReferenceContext} using the text supplied via the {@code append}
|
* Builds a {@link SearchLocationContext} using the text supplied via the {@code append}
|
||||||
* methods.
|
* methods.
|
||||||
* @return the context
|
* @return the context
|
||||||
*/
|
*/
|
||||||
public LocationReferenceContext build() {
|
public SearchLocationContext build() {
|
||||||
return new LocationReferenceContext(parts);
|
return new SearchLocationContext(parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
/* ###
|
||||||
|
* IP: GHIDRA
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package docking.widgets.search;
|
||||||
|
|
||||||
|
import java.net.URL;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.BooleanSupplier;
|
||||||
|
|
||||||
|
import docking.widgets.FindDialog;
|
||||||
|
import docking.widgets.SearchLocation;
|
||||||
|
import ghidra.util.Msg;
|
||||||
|
import ghidra.util.Swing;
|
||||||
|
import ghidra.util.exception.CancelledException;
|
||||||
|
import ghidra.util.task.MonitoredRunnable;
|
||||||
|
import ghidra.util.task.TaskMonitor;
|
||||||
|
import ghidra.util.worker.Job;
|
||||||
|
import ghidra.util.worker.Worker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A collection of {@link SearchLocation}s created when the user has performed a find operation on
|
||||||
|
* the {@link FindDialog}. The dialog will find all results and then use the results to move to the
|
||||||
|
* next and previous locations as requested. The user may also choose to show all results in a
|
||||||
|
* table.
|
||||||
|
* <p>
|
||||||
|
* The searcher uses a worker queue to manage activating and deactivating highlights, which may
|
||||||
|
* require reload operations on the originally searched text.
|
||||||
|
*/
|
||||||
|
public abstract class SearchResults {
|
||||||
|
|
||||||
|
private Worker worker;
|
||||||
|
|
||||||
|
protected SearchResults(Worker worker) {
|
||||||
|
this.worker = worker;
|
||||||
|
}
|
||||||
|
|
||||||
|
Worker getWorker() {
|
||||||
|
return worker;
|
||||||
|
}
|
||||||
|
|
||||||
|
//=================================================================================================
|
||||||
|
// Abstract Methods
|
||||||
|
//=================================================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of this set of search results. This is a short description, such as a
|
||||||
|
* filename or function name. This should be null for text components that do not change
|
||||||
|
* contents based on some external source of data, such as a file.
|
||||||
|
* @return the name or null
|
||||||
|
*/
|
||||||
|
public abstract String getName();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activates this set of search results. This will restore highlights to the source of the
|
||||||
|
* search.
|
||||||
|
*/
|
||||||
|
public abstract void activate();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deactivates this set of search results. This will clear this results' highlights from the
|
||||||
|
* source of the search.
|
||||||
|
*/
|
||||||
|
public abstract void deactivate();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the active location, which will be highlighted differently than the other search
|
||||||
|
* matches. This method will ensure that this search results object is active (see
|
||||||
|
* {@link #activate()}. This method will also move the cursor to the given location.
|
||||||
|
*
|
||||||
|
* @param location the location
|
||||||
|
*/
|
||||||
|
public abstract void setActiveLocation(SearchLocation location);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@return the active search location or null. The active location is typically the search
|
||||||
|
* location that contains the user's cursor.}
|
||||||
|
*/
|
||||||
|
public abstract SearchLocation getActiveLocation();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all search locations in this set of search results
|
||||||
|
* @return the location
|
||||||
|
*/
|
||||||
|
public abstract List<SearchLocation> getLocations();
|
||||||
|
|
||||||
|
public abstract boolean isEmpty();
|
||||||
|
|
||||||
|
public abstract void dispose();
|
||||||
|
|
||||||
|
//=================================================================================================
|
||||||
|
// End Abstract Methods
|
||||||
|
//=================================================================================================
|
||||||
|
|
||||||
|
protected String getFilename(URL url) {
|
||||||
|
if (url == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String path = url.getPath();
|
||||||
|
int index = path.lastIndexOf('/');
|
||||||
|
if (index < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return path.substring(index + 1); // +1 to not get the slash
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all jobs that have the same class as the given job. Clients can call this method
|
||||||
|
* before submitting the given job to clear any other instances of that job type before running.
|
||||||
|
* @param job the job
|
||||||
|
*/
|
||||||
|
protected void cancelAllJobsOfType(FindJob job) {
|
||||||
|
Class<? extends FindJob> clazz = job.getClass();
|
||||||
|
worker.clearAllJobs(j -> j.getClass() == clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the given activation job. This class will cancel any existing activation jobs with the
|
||||||
|
* assumption that only one activation should be taking place at any given time. This is useful
|
||||||
|
* since activations may be slow.
|
||||||
|
* @param job the job
|
||||||
|
*/
|
||||||
|
protected void runActivationJob(ActivationJob job) {
|
||||||
|
cancelAllJobsOfType(job);
|
||||||
|
runJob(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the given job, cancelling any currently running jobs. We assume that only one job
|
||||||
|
* should be run at a time for the given worker for all search results sharing that worker,
|
||||||
|
* not just a single search results. This keeps multiple search results from interfering with
|
||||||
|
* each other as the user interacts with the results.
|
||||||
|
* @param job the job
|
||||||
|
*/
|
||||||
|
protected void runJob(FindJob job) {
|
||||||
|
worker.schedule(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A worker {@link Job} that allows subclasses to add follow-on jobs to be performed as long
|
||||||
|
* as the work is not cancelled.
|
||||||
|
*/
|
||||||
|
protected class FindJob extends Job {
|
||||||
|
|
||||||
|
// The parent job in a chain of jobs. Useful for debugging.
|
||||||
|
protected FindJob parent;
|
||||||
|
|
||||||
|
// optional follow-on job
|
||||||
|
protected FindJob nextJob;
|
||||||
|
|
||||||
|
// optional runnable to be called instead of doRun()
|
||||||
|
protected MonitoredRunnable runnable;
|
||||||
|
|
||||||
|
public FindJob() {
|
||||||
|
// no runnable; use doRun()
|
||||||
|
}
|
||||||
|
|
||||||
|
public FindJob(FindJob parent) {
|
||||||
|
this.parent = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FindJob(FindJob parent, MonitoredRunnable r) {
|
||||||
|
this.parent = parent;
|
||||||
|
this.runnable = r;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void run(TaskMonitor monitor) throws CancelledException {
|
||||||
|
|
||||||
|
monitor.checkCancelled();
|
||||||
|
|
||||||
|
if (runnable != null) {
|
||||||
|
runnable.monitoredRun(monitor);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
doRun(monitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
monitor.checkCancelled();
|
||||||
|
|
||||||
|
if (nextJob != null) {
|
||||||
|
nextJob.run(monitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused") // we don't use the cancel, but subclasses may
|
||||||
|
protected void doRun(TaskMonitor monitor) throws CancelledException {
|
||||||
|
// clients override to do background work
|
||||||
|
}
|
||||||
|
|
||||||
|
public FindJob thenRun(MonitoredRunnable r) {
|
||||||
|
FindJob job = new FindJob(this, r);
|
||||||
|
setNextJob(job);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FindJob thenWait(BooleanSupplier waitFor, Duration maxWaitTime) {
|
||||||
|
FindJob job = new WaitJob(this, waitFor, maxWaitTime);
|
||||||
|
setNextJob(job);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FindJob thenRunSwing(Runnable r) {
|
||||||
|
MonitoredRunnable swingRunnable = m -> {
|
||||||
|
Swing.runNow(() -> {
|
||||||
|
if (m.isCancelled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
r.run();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
FindJob job = new FindJob(this, swingRunnable);
|
||||||
|
setNextJob(job);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setNextJob(FindJob job) {
|
||||||
|
if (nextJob == null) {
|
||||||
|
nextJob = job;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
nextJob.setNextJob(job);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
String base = getClass().getSimpleName() + ' ' + SearchResults.this;
|
||||||
|
if (runnable != null) {
|
||||||
|
return "runnable-only " + base;
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ActivationJob extends FindJob {
|
||||||
|
// nothing special to do here; just a marker class
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SwingJob extends FindJob {
|
||||||
|
public SwingJob(Runnable r) {
|
||||||
|
this.runnable = m -> r.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class WaitJob extends FindJob {
|
||||||
|
private BooleanSupplier waitFor;
|
||||||
|
private Duration maxWaitTime;
|
||||||
|
|
||||||
|
protected WaitJob(FindJob parent, BooleanSupplier waitFor, Duration maxWaitTime) {
|
||||||
|
super(parent);
|
||||||
|
this.waitFor = waitFor;
|
||||||
|
this.maxWaitTime = maxWaitTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doRun(TaskMonitor monitor) throws CancelledException {
|
||||||
|
int sleepyTime = 250;
|
||||||
|
int totalMs = 0;
|
||||||
|
while (totalMs < maxWaitTime.toMillis()) {
|
||||||
|
|
||||||
|
monitor.checkCancelled();
|
||||||
|
|
||||||
|
if (waitFor.getAsBoolean()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalMs += sleepyTime;
|
||||||
|
sleep(sleepyTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
monitor.cancel();
|
||||||
|
throw new CancelledException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sleep(int sleepyTime) throws CancelledException {
|
||||||
|
|
||||||
|
try {
|
||||||
|
Thread.sleep(sleepyTime);
|
||||||
|
}
|
||||||
|
catch (InterruptedException e) {
|
||||||
|
Msg.debug(this, "Find job interrupted while waiting");
|
||||||
|
throw new CancelledException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/* ###
|
||||||
|
* IP: GHIDRA
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package docking.widgets.search;
|
||||||
|
|
||||||
|
import docking.widgets.SearchLocation;
|
||||||
|
|
||||||
|
public class TextComponentSearchLocation extends SearchLocation {
|
||||||
|
|
||||||
|
private boolean isActive;
|
||||||
|
private Object lastHighlightTag;
|
||||||
|
|
||||||
|
TextComponentSearchLocation(String searchText, int startInclusive, int endInclusive,
|
||||||
|
int lineNumber, SearchLocationContext context) {
|
||||||
|
super(startInclusive, endInclusive, searchText, lineNumber, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setActive(boolean b) {
|
||||||
|
isActive = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isActive() {
|
||||||
|
return isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sets the Highlighter created that allows for removal of the highlight for this match
|
||||||
|
void setHighlightTag(Object tag) {
|
||||||
|
this.lastHighlightTag = tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object getHighlightTag() {
|
||||||
|
return lastHighlightTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,616 @@
|
|||||||
|
/* ###
|
||||||
|
* IP: GHIDRA
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package docking.widgets.search;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
|
import javax.swing.JEditorPane;
|
||||||
|
import javax.swing.event.*;
|
||||||
|
import javax.swing.text.*;
|
||||||
|
import javax.swing.text.DefaultHighlighter.DefaultHighlightPainter;
|
||||||
|
import javax.swing.text.Highlighter.Highlight;
|
||||||
|
import javax.swing.text.Highlighter.HighlightPainter;
|
||||||
|
|
||||||
|
import docking.widgets.SearchLocation;
|
||||||
|
import generic.theme.GColor;
|
||||||
|
import ghidra.util.Msg;
|
||||||
|
import ghidra.util.Swing;
|
||||||
|
import ghidra.util.task.SwingUpdateManager;
|
||||||
|
import ghidra.util.worker.Worker;
|
||||||
|
import util.CollectionUtils;
|
||||||
|
|
||||||
|
public class TextComponentSearchResults extends SearchResults {
|
||||||
|
|
||||||
|
private Color highlightColor = new GColor("color.bg.find.highlight");
|
||||||
|
private Color activeHighlightColor = new GColor("color.bg.find.highlight.active");
|
||||||
|
|
||||||
|
protected JEditorPane editorPane;
|
||||||
|
private SearchResultsHighlighterWrapper highlighter;
|
||||||
|
private DocumentListener documentListener = new DocumentChangeListener();
|
||||||
|
private CaretListener caretListener = new CaretChangeListener();
|
||||||
|
private SwingUpdateManager caretUpdater =
|
||||||
|
new SwingUpdateManager(() -> setActiveHighlightBasedOnCaret());
|
||||||
|
private boolean isUpdatingCaretInternally;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
private List<TextComponentSearchLocation> searchLocations;
|
||||||
|
private TreeMap<Integer, TextComponentSearchLocation> matchesByPosition;
|
||||||
|
private String searchText;
|
||||||
|
private TextComponentSearchLocation activeLocation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stale means the document has changed and our location offsets may no longer match. Once
|
||||||
|
* stale, always stale.
|
||||||
|
*/
|
||||||
|
private boolean isStale;
|
||||||
|
|
||||||
|
protected TextComponentSearchResults(Worker worker, JEditorPane editorPane, String searchText,
|
||||||
|
TreeMap<Integer, TextComponentSearchLocation> matchesByPosition) {
|
||||||
|
super(worker);
|
||||||
|
this.editorPane = editorPane;
|
||||||
|
this.searchText = searchText;
|
||||||
|
this.matchesByPosition = matchesByPosition;
|
||||||
|
|
||||||
|
URL url = editorPane.getPage();
|
||||||
|
this.name = getFilename(url);
|
||||||
|
|
||||||
|
Collection<TextComponentSearchLocation> matches = matchesByPosition.values();
|
||||||
|
this.searchLocations = new ArrayList<>(matches);
|
||||||
|
|
||||||
|
Document document = editorPane.getDocument();
|
||||||
|
document.addDocumentListener(documentListener);
|
||||||
|
|
||||||
|
editorPane.addCaretListener(caretListener);
|
||||||
|
|
||||||
|
// All results will be highlighted. Since we don't move the caret to a specific match, make
|
||||||
|
// sure that the highlight color gets updated based on the current caret position.
|
||||||
|
caretUpdater.updateLater();
|
||||||
|
|
||||||
|
highlighter = createHighlighter();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deactivate() {
|
||||||
|
if (isActive()) {
|
||||||
|
FindJob job = new SwingJob(this::unapplyHighlights);
|
||||||
|
runJob(job);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers the potentially asynchronous activation of this set of search results. When that is
|
||||||
|
* finished, we then restore our highlights. This is needed in the case that the implementor
|
||||||
|
* is using a document that does not match our search results. Some subclasses use
|
||||||
|
* asynchronous loading of their document.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void activate() {
|
||||||
|
FindJob job = startActivation();
|
||||||
|
runJob(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setActiveLocation(SearchLocation location) {
|
||||||
|
if (isStale) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeLocation == location) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (location == null) {
|
||||||
|
// no need to activate these results when clearing the active location
|
||||||
|
Swing.runNow(() -> doSetActiveLocation(null));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FindJob job = startActivation().thenRunSwing(() -> doSetActiveLocation(location));
|
||||||
|
runActivationJob((ActivationJob) job);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a job to perform activation for this class. The activation job may be a 'done' job
|
||||||
|
* if not activation is required.
|
||||||
|
* @return the job
|
||||||
|
*/
|
||||||
|
protected ActivationJob startActivation() {
|
||||||
|
if (isActive()) {
|
||||||
|
return createFinishedActivationJob();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStale) {
|
||||||
|
unapplyHighlights();
|
||||||
|
return createFinishedActivationJob();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (ActivationJob) createActivationJob().thenRunSwing(() -> applyHighlights());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the job that will activate this class. Subclasses can override this method to change
|
||||||
|
* the job that gets run.
|
||||||
|
* @return the job
|
||||||
|
*/
|
||||||
|
protected ActivationJob createActivationJob() {
|
||||||
|
return new ActivationJob();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ActivationJob createFinishedActivationJob() {
|
||||||
|
return new ActivationJob();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doSetActiveLocation(SearchLocation newLocation) {
|
||||||
|
TextComponentSearchLocation oldLocation = activeLocation;
|
||||||
|
activeLocation = (TextComponentSearchLocation) newLocation;
|
||||||
|
if (oldLocation == newLocation) {
|
||||||
|
scrollToLocation(activeLocation); // this handles null
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeActiveLocation(oldLocation, activeLocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextComponentSearchLocation getNextLocation(int searchStart, boolean searchForward) {
|
||||||
|
|
||||||
|
Entry<Integer, TextComponentSearchLocation> entry;
|
||||||
|
if (searchForward) {
|
||||||
|
entry = matchesByPosition.ceilingEntry(searchStart);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
entry = matchesByPosition.floorEntry(searchStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry == null) {
|
||||||
|
return null; // no more matches in the current direction
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return searchLocations.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SearchLocation> getLocations() {
|
||||||
|
return CollectionUtils.asList(searchLocations, SearchLocation.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isStale() {
|
||||||
|
return isStale;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setStale() {
|
||||||
|
isStale = true;
|
||||||
|
unapplyHighlights();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isInvalid(String otherSearchText) {
|
||||||
|
if (isStale) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !searchText.equals(otherSearchText);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SearchLocation getActiveLocation() {
|
||||||
|
return activeLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateActiveLocationForCaretChange(int caret) {
|
||||||
|
TextComponentSearchLocation location = getLocation(caret);
|
||||||
|
setActiveLocation(location);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextComponentSearchLocation getLocation(int caret) {
|
||||||
|
Optional<TextComponentSearchLocation> optional =
|
||||||
|
searchLocations.stream().filter(l -> l.contains(caret)).findFirst();
|
||||||
|
return optional.orElseGet(() -> null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void changeActiveLocation(TextComponentSearchLocation oldLocation,
|
||||||
|
TextComponentSearchLocation newLocation) {
|
||||||
|
|
||||||
|
clearActiveHighlight(oldLocation);
|
||||||
|
|
||||||
|
if (isStale) {
|
||||||
|
// no way to easily change highlights for the caret position while we are stale,
|
||||||
|
// since the locations no longer match the document positions
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newLocation == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newLocation.setActive(true);
|
||||||
|
doHighlightLocation(newLocation);
|
||||||
|
scrollToLocation(newLocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearActiveHighlight(TextComponentSearchLocation location) {
|
||||||
|
|
||||||
|
if (location == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
location.setActive(false);
|
||||||
|
doHighlightLocation(location); // turn off the active highlight
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scrollToLocation(TextComponentSearchLocation location) {
|
||||||
|
if (location == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int caret = editorPane.getCaretPosition();
|
||||||
|
if (!location.contains(caret)) {
|
||||||
|
setCaretPositionInternally(location);
|
||||||
|
}
|
||||||
|
scrollToVisible(location);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setCaretPositionInternally(TextComponentSearchLocation location) {
|
||||||
|
|
||||||
|
if (isStale) {
|
||||||
|
// once the document contents have changed, we have know way of knowing if the matches
|
||||||
|
// are still valid
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Document doc = editorPane.getDocument();
|
||||||
|
int len = doc.getLength();
|
||||||
|
if (len == 0) {
|
||||||
|
// This can happen if the document is getting loaded asynchronously. We work around
|
||||||
|
// this elsewhere, making this a very low occurrence event. If it happens, just
|
||||||
|
// ignore the caret update.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isUpdatingCaretInternally = true;
|
||||||
|
try {
|
||||||
|
int pos = location.getStartIndexInclusive();
|
||||||
|
editorPane.setCaretPosition(pos);
|
||||||
|
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
isUpdatingCaretInternally = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scrollToVisible(TextComponentSearchLocation location) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
int start = location.getStartIndexInclusive();
|
||||||
|
int end = location.getEndIndexInclusive();
|
||||||
|
Rectangle startR = editorPane.modelToView2D(start).getBounds();
|
||||||
|
Rectangle endR = editorPane.modelToView2D(end).getBounds();
|
||||||
|
endR.width += 20; // a little extra space so the view is not right at the text end
|
||||||
|
Rectangle union = startR.union(endR);
|
||||||
|
editorPane.scrollRectToVisible(union);
|
||||||
|
}
|
||||||
|
catch (BadLocationException e) {
|
||||||
|
Msg.debug(this, "Exception scrolling to text", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setActiveHighlightBasedOnCaret() {
|
||||||
|
if (!isActive()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int pos = editorPane.getCaretPosition();
|
||||||
|
updateActiveLocationForCaretChange(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates our search highlighter, wrapping any existing highlighter in order to not lose
|
||||||
|
* client highlights.
|
||||||
|
* @return the new highlighter
|
||||||
|
*/
|
||||||
|
private SearchResultsHighlighterWrapper createHighlighter() {
|
||||||
|
|
||||||
|
Highlighter activeHighlighter = editorPane.getHighlighter();
|
||||||
|
if (activeHighlighter == null) {
|
||||||
|
return new SearchResultsHighlighterWrapper(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeHighlighter instanceof SearchResultsHighlighterWrapper wrapper) {
|
||||||
|
// don't wrap another search highlighter, as we will check for them later
|
||||||
|
return new SearchResultsHighlighterWrapper(wrapper.delegate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// some other client non-search highlighter
|
||||||
|
return new SearchResultsHighlighterWrapper(activeHighlighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SearchResultsHighlighterWrapper getInstalledSearchResultsHighlighter() {
|
||||||
|
|
||||||
|
Highlighter activeHighlighter = editorPane.getHighlighter();
|
||||||
|
if (activeHighlighter == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeHighlighter instanceof SearchResultsHighlighterWrapper wrapper) {
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
// some other client non-search highlighter
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
We used to use a boolean to track the active state. However, due to how clients activate
|
||||||
|
and deactivate, the boolean could get out-of-sync with the highlighter. Thus, use the
|
||||||
|
active highlighter as the method for checking if we are active.
|
||||||
|
*/
|
||||||
|
boolean isActive() {
|
||||||
|
SearchResultsHighlighterWrapper activeSearchHighlighter =
|
||||||
|
getInstalledSearchResultsHighlighter();
|
||||||
|
|
||||||
|
return activeSearchHighlighter == highlighter;
|
||||||
|
}
|
||||||
|
|
||||||
|
Highlight[] getHighlights() {
|
||||||
|
return highlighter.getHighlights();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void maybeInstallHighlighter() {
|
||||||
|
|
||||||
|
SearchResultsHighlighterWrapper activeHighlighter =
|
||||||
|
getInstalledSearchResultsHighlighter();
|
||||||
|
|
||||||
|
if (activeHighlighter == highlighter) {
|
||||||
|
// we are already installed
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeHighlighter != null) {
|
||||||
|
// another search highlighter is installed
|
||||||
|
activeHighlighter.removeAllHighlights();
|
||||||
|
}
|
||||||
|
|
||||||
|
editorPane.setHighlighter(highlighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyHighlights() {
|
||||||
|
|
||||||
|
unapplyHighlights();
|
||||||
|
|
||||||
|
// Any other search highlights will be cleared when we install our highlighter
|
||||||
|
maybeInstallHighlighter();
|
||||||
|
|
||||||
|
Collection<TextComponentSearchLocation> locations = matchesByPosition.values();
|
||||||
|
for (TextComponentSearchLocation location : locations) {
|
||||||
|
doHighlightLocation(location);
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveHighlightBasedOnCaret();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears highlights, but does not remove known matches. This allows highlights to later be
|
||||||
|
* restored.
|
||||||
|
*/
|
||||||
|
private void unapplyHighlights() {
|
||||||
|
|
||||||
|
// reset and repaint the active highlight
|
||||||
|
setActiveLocation(null);
|
||||||
|
|
||||||
|
Highlighter activeHighlighter = editorPane.getHighlighter();
|
||||||
|
if (activeHighlighter == highlighter) {
|
||||||
|
// only remove our highlights
|
||||||
|
highlighter.removeAllHighlights();
|
||||||
|
highlighter.uninstall();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doHighlightLocation(TextComponentSearchLocation location) {
|
||||||
|
|
||||||
|
Highlighter activeHighlighter = editorPane.getHighlighter();
|
||||||
|
if (activeHighlighter != highlighter) {
|
||||||
|
// Not our highlighter; don't change highlights. Shouldn't happen.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object tag = location.getHighlightTag();
|
||||||
|
if (tag != null) {
|
||||||
|
// always remove any previous highlight before adding a new one
|
||||||
|
highlighter.removeHighlight(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStale) {
|
||||||
|
return; // do not highlight when stale
|
||||||
|
}
|
||||||
|
|
||||||
|
Color c = location.isActive() ? activeHighlightColor : highlightColor;
|
||||||
|
HighlightPainter painter = new DefaultHighlightPainter(c);
|
||||||
|
int start = location.getStartIndexInclusive();
|
||||||
|
int end = location.getEndIndexInclusive() + 1; // +1 to make inclusive be exclusive
|
||||||
|
try {
|
||||||
|
tag = highlighter.addHighlight(start, end, painter);
|
||||||
|
location.setHighlightTag(tag);
|
||||||
|
}
|
||||||
|
catch (BadLocationException e) {
|
||||||
|
Msg.debug(this, "Exception adding highlight", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispose() {
|
||||||
|
deactivate();
|
||||||
|
caretUpdater.dispose();
|
||||||
|
|
||||||
|
if (editorPane != null) {
|
||||||
|
Document document = editorPane.getDocument();
|
||||||
|
document.removeDocumentListener(documentListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
matchesByPosition.clear();
|
||||||
|
searchLocations.clear();
|
||||||
|
|
||||||
|
highlighter.uninstall();
|
||||||
|
isStale = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "%s: %s (%s)".formatted(getClass().getSimpleName(), searchText,
|
||||||
|
System.identityHashCode(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
//=================================================================================================
|
||||||
|
// Inner Classes
|
||||||
|
//=================================================================================================
|
||||||
|
|
||||||
|
private class DocumentChangeListener implements DocumentListener {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void insertUpdate(DocumentEvent e) {
|
||||||
|
// this allows the previous search results to stay visible until a new find is requested
|
||||||
|
setStale();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeUpdate(DocumentEvent e) {
|
||||||
|
setStale();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void changedUpdate(DocumentEvent e) {
|
||||||
|
// ignore attribute changes since they don't affect the text content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CaretChangeListener implements CaretListener {
|
||||||
|
|
||||||
|
private int lastPos = -1;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void caretUpdate(CaretEvent e) {
|
||||||
|
int pos = e.getDot();
|
||||||
|
if (isUpdatingCaretInternally) {
|
||||||
|
lastPos = pos;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos == lastPos) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastPos = pos;
|
||||||
|
caretUpdater.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class that allows us to replace any already installed highlighter. This also allows us to
|
||||||
|
* add and remove highlighters, depending upon the active search.
|
||||||
|
* <p>
|
||||||
|
* Note: any non-search highlighters installed after this wrapper is created may be overwritten
|
||||||
|
* as the usr interacts with the search.
|
||||||
|
*/
|
||||||
|
private class SearchResultsHighlighterWrapper extends DefaultHighlighter {
|
||||||
|
|
||||||
|
private Highlighter delegate;
|
||||||
|
private boolean nonSearchDelegate;
|
||||||
|
|
||||||
|
SearchResultsHighlighterWrapper(Highlighter delegate) {
|
||||||
|
if (delegate == null) {
|
||||||
|
delegate = new DefaultHighlighter();
|
||||||
|
nonSearchDelegate = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
nonSearchDelegate = true;
|
||||||
|
}
|
||||||
|
this.delegate = delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
void uninstall() {
|
||||||
|
Highlighter activeHighlighter = editorPane.getHighlighter();
|
||||||
|
if (activeHighlighter != this) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nonSearchDelegate) {
|
||||||
|
editorPane.setHighlighter(delegate);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
editorPane.setHighlighter(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void install(JTextComponent c) {
|
||||||
|
delegate.install(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deinstall(JTextComponent c) {
|
||||||
|
delegate.deinstall(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void paint(Graphics g) {
|
||||||
|
delegate.paint(g);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void paintLayeredHighlights(Graphics g, int p0, int p1, Shape viewBounds,
|
||||||
|
JTextComponent editor, View view) {
|
||||||
|
if (delegate instanceof LayeredHighlighter lh) {
|
||||||
|
lh.paintLayeredHighlights(g, p0, p1, viewBounds, editor, view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object addHighlight(int p0, int p1, HighlightPainter p) throws BadLocationException {
|
||||||
|
return delegate.addHighlight(p0, p1, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeHighlight(Object tag) {
|
||||||
|
delegate.removeHighlight(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeAllHighlights() {
|
||||||
|
delegate.removeAllHighlights();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void changeHighlight(Object tag, int p0, int p1) throws BadLocationException {
|
||||||
|
delegate.changeHighlight(tag, p0, p1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Highlight[] getHighlights() {
|
||||||
|
return delegate.getHighlights();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
/* ###
|
||||||
|
* IP: GHIDRA
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package docking.widgets.search;
|
||||||
|
|
||||||
|
import java.awt.TextComponent;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.regex.*;
|
||||||
|
|
||||||
|
import javax.swing.JEditorPane;
|
||||||
|
import javax.swing.text.BadLocationException;
|
||||||
|
import javax.swing.text.Document;
|
||||||
|
|
||||||
|
import docking.widgets.CursorPosition;
|
||||||
|
import docking.widgets.SearchLocation;
|
||||||
|
import ghidra.util.Msg;
|
||||||
|
import ghidra.util.UserSearchUtils;
|
||||||
|
import ghidra.util.exception.CancelledException;
|
||||||
|
import ghidra.util.task.*;
|
||||||
|
import ghidra.util.worker.Worker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class to find text matches in the given {@link TextComponent}. This class will search for all
|
||||||
|
* matches and cache the results for future requests when the user presses Next or Previous. All
|
||||||
|
* matches will be highlighted in the text component. The match containing the cursor will be a
|
||||||
|
* different highlight color than the others. When the find dialog is closed, all highlights are
|
||||||
|
* removed.
|
||||||
|
* <p>
|
||||||
|
* If {@link #searchAll(String, boolean)} is called, then the search results will not be cached, as
|
||||||
|
* they are when {@link #search(String, CursorPosition, boolean, boolean)} is used. The expectation
|
||||||
|
* is that clients will cache the search results themselves.
|
||||||
|
*/
|
||||||
|
public class TextComponentSearcher implements FindDialogSearcher {
|
||||||
|
|
||||||
|
static final int MAX_CONTEXT_CHARS = 100;
|
||||||
|
private int maxContextChars = MAX_CONTEXT_CHARS;
|
||||||
|
|
||||||
|
protected JEditorPane editorPane;
|
||||||
|
|
||||||
|
private Worker worker = Worker.createGuiWorker();
|
||||||
|
private TextComponentSearchResults searchResults;
|
||||||
|
|
||||||
|
public TextComponentSearcher(JEditorPane editorPane) {
|
||||||
|
this.editorPane = editorPane;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEditorPane(JEditorPane editorPane) {
|
||||||
|
if (this.editorPane != editorPane) {
|
||||||
|
if (searchResults != null) {
|
||||||
|
searchResults.dispose();
|
||||||
|
searchResults = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.editorPane = editorPane;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JEditorPane getEditorPane() {
|
||||||
|
return editorPane;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setMaxContextChars(int max) {
|
||||||
|
this.maxContextChars = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispose() {
|
||||||
|
|
||||||
|
if (searchResults != null) {
|
||||||
|
searchResults.dispose();
|
||||||
|
searchResults = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasSearchResults() {
|
||||||
|
return searchResults != null && !searchResults.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isStale() {
|
||||||
|
return searchResults != null && searchResults.isStale();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CursorPosition getCursorPosition() {
|
||||||
|
int pos = editorPane.getCaretPosition();
|
||||||
|
return new CursorPosition(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CursorPosition getStart() {
|
||||||
|
return new CursorPosition(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CursorPosition getEnd() {
|
||||||
|
int length = editorPane.getDocument().getLength();
|
||||||
|
return new CursorPosition(length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TextComponentSearchResults searchAll(String text, boolean useRegex) {
|
||||||
|
return doSearch(text, useRegex);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SearchResults search(String text, CursorPosition cursorPosition,
|
||||||
|
boolean searchForward, boolean useRegex) {
|
||||||
|
|
||||||
|
updateSearchResults(text, useRegex);
|
||||||
|
|
||||||
|
int pos = cursorPosition.getPosition();
|
||||||
|
int searchStart = getSearchStart(pos, searchForward);
|
||||||
|
if (searchStart == -1) {
|
||||||
|
return null; // signal no more matches in the current direction
|
||||||
|
}
|
||||||
|
|
||||||
|
TextComponentSearchLocation location =
|
||||||
|
searchResults.getNextLocation(searchStart, searchForward);
|
||||||
|
if (location == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchResults.setActiveLocation(location);
|
||||||
|
return searchResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSearchResults(String text, boolean useRegex) {
|
||||||
|
if (searchResults != null) {
|
||||||
|
if (!searchResults.isInvalid(text)) {
|
||||||
|
|
||||||
|
// the current results are still valid; ensure the highlights are still active
|
||||||
|
searchResults.activate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchResults.dispose();
|
||||||
|
searchResults = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchResults = doSearch(text, useRegex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextComponentSearchResults doSearch(String text, boolean useRegex) {
|
||||||
|
SearchTask searchTask = new SearchTask(text, useRegex);
|
||||||
|
TaskLauncher.launch(searchTask);
|
||||||
|
|
||||||
|
TextComponentSearchResults newSearchResults = searchTask.doCreateSearchResults();
|
||||||
|
newSearchResults.activate();
|
||||||
|
return newSearchResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getSearchStart(int startPosition, boolean isForward) {
|
||||||
|
|
||||||
|
SearchLocation location = searchResults.getActiveLocation();
|
||||||
|
if (location == null) {
|
||||||
|
return startPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
int lastMatchStart = location.getStartIndexInclusive();
|
||||||
|
if (startPosition != lastMatchStart) {
|
||||||
|
return startPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always prefer the caret position, unless it aligns with the previous match. By
|
||||||
|
// moving it forward one we will continue our search, as opposed to always matching
|
||||||
|
// the same hit.
|
||||||
|
if (isForward) {
|
||||||
|
int next = startPosition + 1;
|
||||||
|
int end = editorPane.getText().length();
|
||||||
|
if (next == end) {
|
||||||
|
return -1; // signal no more hits in this direction
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
// backwards
|
||||||
|
if (startPosition == 0) {
|
||||||
|
return -1; // signal no more hits in this direction
|
||||||
|
}
|
||||||
|
return startPosition - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected TextComponentSearchResults createSearchResults(
|
||||||
|
Worker theWorker, JEditorPane editor, String searchText,
|
||||||
|
TreeMap<Integer, TextComponentSearchLocation> matchesByPosition) {
|
||||||
|
return new TextComponentSearchResults(theWorker, editor, searchText, matchesByPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
//=================================================================================================
|
||||||
|
// Inner Classes
|
||||||
|
//=================================================================================================
|
||||||
|
|
||||||
|
private class SearchTask extends Task {
|
||||||
|
|
||||||
|
private String searchText;
|
||||||
|
private TreeMap<Integer, TextComponentSearchLocation> matchesByPosition = new TreeMap<>();
|
||||||
|
private boolean useRegex;
|
||||||
|
|
||||||
|
SearchTask(String searchText, boolean useRegex) {
|
||||||
|
super("Help Search Task", true, false, true, true);
|
||||||
|
this.searchText = searchText;
|
||||||
|
this.useRegex = useRegex;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextComponentSearchResults doCreateSearchResults() {
|
||||||
|
return createSearchResults(worker, editorPane, searchText, matchesByPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(TaskMonitor monitor) throws CancelledException {
|
||||||
|
|
||||||
|
Document document;
|
||||||
|
String fullText;
|
||||||
|
try {
|
||||||
|
document = editorPane.getDocument();
|
||||||
|
fullText = document.getText(0, document.getLength());
|
||||||
|
}
|
||||||
|
catch (BadLocationException e) {
|
||||||
|
Msg.error(this, "Unable to get text for user find operation", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TreeMap<Integer, Line> lineRangeMap = mapLines(fullText);
|
||||||
|
|
||||||
|
Pattern pattern = createSearchPattern(searchText, useRegex);
|
||||||
|
Matcher matcher = pattern.matcher(fullText);
|
||||||
|
while (matcher.find()) {
|
||||||
|
monitor.checkCancelled();
|
||||||
|
int start = matcher.start();
|
||||||
|
int end = matcher.end();
|
||||||
|
Line line = lineRangeMap.floorEntry(start).getValue();
|
||||||
|
|
||||||
|
String matchText = fullText.substring(start, end);
|
||||||
|
SearchLocationContext context = createContext(line, start, end);
|
||||||
|
TextComponentSearchLocation location =
|
||||||
|
new TextComponentSearchLocation(matchText, start, end - 1, line.lineNumber(),
|
||||||
|
context);
|
||||||
|
matchesByPosition.put(start, location);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private TreeMap<Integer, Line> mapLines(String fullText) {
|
||||||
|
TreeMap<Integer, Line> linesRangeMap = new TreeMap<>();
|
||||||
|
int lineNumber = 0;
|
||||||
|
int pos = 0;
|
||||||
|
String[] lines = fullText.split("\\n");
|
||||||
|
for (String line : lines) {
|
||||||
|
lineNumber++;
|
||||||
|
linesRangeMap.put(pos, new Line(line, lineNumber, pos));
|
||||||
|
pos += line.length() + 1; // +1 for newline
|
||||||
|
}
|
||||||
|
return linesRangeMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SearchLocationContext createContext(Line line, int start, int end) {
|
||||||
|
SearchLocationContextBuilder builder = new SearchLocationContextBuilder();
|
||||||
|
String text = line.text();
|
||||||
|
int offset = line.offset(); // document offset
|
||||||
|
int rstart = start - offset; // line-relative start
|
||||||
|
int rend = end - offset; // line-relative end
|
||||||
|
int lineStart = 0;
|
||||||
|
int lineEnd = text.length();
|
||||||
|
|
||||||
|
int length = text.length();
|
||||||
|
int max = maxContextChars;
|
||||||
|
if (length > max) {
|
||||||
|
// HTML content can have very long lines, since it doesn't use newline characters to
|
||||||
|
// break text. We just want to show some context, so we don't need all characters.
|
||||||
|
// When the text is too long, just grab some surrounding text for the context.
|
||||||
|
int matchLength = end - start;
|
||||||
|
int remaining = max - matchLength;
|
||||||
|
int half = remaining / 2;
|
||||||
|
int firstHalf = rstart; // from 0 to match start
|
||||||
|
int available = Math.min(half, firstHalf);
|
||||||
|
int newStart = rstart - available;
|
||||||
|
|
||||||
|
available = max - (available + matchLength);
|
||||||
|
int newEnd = Math.min(length, rend + available);
|
||||||
|
|
||||||
|
lineStart = newStart;
|
||||||
|
lineEnd = newEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lineStart != 0) {
|
||||||
|
builder.append("...");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.append(text.substring(lineStart, rstart));
|
||||||
|
builder.appendMatch(text.substring(rstart, rend));
|
||||||
|
if (rend < text.length()) {
|
||||||
|
builder.append(text.substring(rend, lineEnd));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lineEnd < text.length()) {
|
||||||
|
builder.append("...");
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Pattern createSearchPattern(String searchString, boolean isRegex) {
|
||||||
|
|
||||||
|
int options = Pattern.CASE_INSENSITIVE | Pattern.DOTALL;
|
||||||
|
if (isRegex) {
|
||||||
|
try {
|
||||||
|
return Pattern.compile(searchString, options);
|
||||||
|
}
|
||||||
|
catch (PatternSyntaxException e) {
|
||||||
|
Msg.showError(this, editorPane, "Regular Expression Syntax Error",
|
||||||
|
e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return UserSearchUtils.createPattern(searchString, false, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
record Line(String text, int lineNumber, int offset) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -41,7 +41,7 @@ public class DockingTabRenderer extends JPanel {
|
|||||||
private TabContainerForwardingMouseListener forwardingListener;
|
private TabContainerForwardingMouseListener forwardingListener;
|
||||||
private MouseListener renameListener;
|
private MouseListener renameListener;
|
||||||
|
|
||||||
public DockingTabRenderer(final JTabbedPane tabbedPane, String fullTitle, String tabTitle,
|
public DockingTabRenderer(final JTabbedPane tabbedPane, String fullTitle, String tabText,
|
||||||
ActionListener closeListener) {
|
ActionListener closeListener) {
|
||||||
|
|
||||||
final ForwardingMouseListener eventForwardingListener =
|
final ForwardingMouseListener eventForwardingListener =
|
||||||
@@ -51,8 +51,8 @@ public class DockingTabRenderer extends JPanel {
|
|||||||
iconLabel = new GDLabel();
|
iconLabel = new GDLabel();
|
||||||
closeButton = new EmptyBorderButton();
|
closeButton = new EmptyBorderButton();
|
||||||
|
|
||||||
setTitle(tabTitle, fullTitle);
|
setTitle(tabText, fullTitle);
|
||||||
closeButton.setToolTipText("Close " + tabTitle);
|
closeButton.setToolTipText("Close " + tabText);
|
||||||
closeButton.setFocusable(false);
|
closeButton.setFocusable(false);
|
||||||
closeButton.addActionListener(closeListener);
|
closeButton.addActionListener(closeListener);
|
||||||
closeButton.setIcon(CLOSE_ICON);
|
closeButton.setIcon(CLOSE_ICON);
|
||||||
@@ -134,13 +134,13 @@ public class DockingTabRenderer extends JPanel {
|
|||||||
iconLabel.setIcon(icon);
|
iconLabel.setIcon(icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setTitle(String tabTitle, String fullTitle) {
|
public void setTitle(String tabText, String fullTitle) {
|
||||||
titleLabel.setText(getShortenedTitle(tabTitle));
|
titleLabel.setText(getShortenedTitle(tabText));
|
||||||
String trimmedTabText = tabTitle.trim();
|
String trimmedTabText = tabText.trim();
|
||||||
String trimmedTitleText = fullTitle.trim();
|
String trimmedTitleText = fullTitle.trim();
|
||||||
if (trimmedTabText.equals(trimmedTitleText)) {
|
if (trimmedTabText.equals(trimmedTitleText)) {
|
||||||
// don't include the same text on twice
|
// don't include the same text on twice
|
||||||
titleLabel.setToolTipText(tabTitle);
|
titleLabel.setToolTipText(tabText);
|
||||||
}
|
}
|
||||||
else if (trimmedTitleText.contains(trimmedTabText)) {
|
else if (trimmedTitleText.contains(trimmedTabText)) {
|
||||||
// don't include both when the tab text is a subset of the title
|
// don't include both when the tab text is a subset of the title
|
||||||
@@ -148,7 +148,7 @@ public class DockingTabRenderer extends JPanel {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// both are different, include both
|
// both are different, include both
|
||||||
titleLabel.setToolTipText("<html><b>" + tabTitle + "</b> - [" + fullTitle + "]");
|
titleLabel.setToolTipText("<html><b>" + tabText + "</b> - [" + fullTitle + "]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package ghidra.util.table.actions;
|
package docking.widgets.table.actions;
|
||||||
|
|
||||||
import java.awt.event.KeyEvent;
|
import java.awt.event.KeyEvent;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -24,14 +24,13 @@ import javax.swing.KeyStroke;
|
|||||||
import javax.swing.table.TableModel;
|
import javax.swing.table.TableModel;
|
||||||
|
|
||||||
import docking.ActionContext;
|
import docking.ActionContext;
|
||||||
|
import docking.Tool;
|
||||||
import docking.action.*;
|
import docking.action.*;
|
||||||
import docking.actions.SharedDockingActionPlaceholder;
|
import docking.actions.SharedDockingActionPlaceholder;
|
||||||
import docking.widgets.table.GTable;
|
import docking.widgets.table.GTable;
|
||||||
import docking.widgets.table.RowObjectTableModel;
|
import docking.widgets.table.RowObjectTableModel;
|
||||||
import docking.widgets.table.threaded.ThreadedTableModel;
|
import docking.widgets.table.threaded.ThreadedTableModel;
|
||||||
import generic.theme.GIcon;
|
import generic.theme.GIcon;
|
||||||
import ghidra.app.util.HelpTopics;
|
|
||||||
import ghidra.framework.plugintool.PluginTool;
|
|
||||||
import ghidra.util.*;
|
import ghidra.util.*;
|
||||||
import ghidra.util.exception.AssertException;
|
import ghidra.util.exception.AssertException;
|
||||||
import ghidra.util.timer.GTimer;
|
import ghidra.util.timer.GTimer;
|
||||||
@@ -46,7 +45,7 @@ import ghidra.util.timer.GTimer;
|
|||||||
* not altering the database.
|
* not altering the database.
|
||||||
* <p>
|
* <p>
|
||||||
* Tip: if you are a plugin that uses transient providers, then use
|
* Tip: if you are a plugin that uses transient providers, then use
|
||||||
* {@link #registerDummy(PluginTool, String)} at creation time to install a dummy representative of
|
* {@link #registerDummy(Tool, String)} at creation time to install a dummy representative of
|
||||||
* this action in the Tool's options so that user's can update keybindings, regardless of whether
|
* this action in the Tool's options so that user's can update keybindings, regardless of whether
|
||||||
* they have ever shown one of your transient providers.
|
* they have ever shown one of your transient providers.
|
||||||
*/
|
*/
|
||||||
@@ -54,7 +53,7 @@ public class DeleteTableRowAction extends DockingAction {
|
|||||||
|
|
||||||
private static final KeyStroke DEFAULT_KEYSTROKE =
|
private static final KeyStroke DEFAULT_KEYSTROKE =
|
||||||
KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0);
|
KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0);
|
||||||
private static final Icon ICON = new GIcon("icon.plugin.table.delete.row");
|
private static final Icon ICON = new GIcon("icon.widget.table.delete.row");
|
||||||
private static final String NAME = "Remove Items";
|
private static final String NAME = "Remove Items";
|
||||||
|
|
||||||
private GTable table;
|
private GTable table;
|
||||||
@@ -67,7 +66,7 @@ public class DeleteTableRowAction extends DockingAction {
|
|||||||
* @param tool the tool whose options will updated with a dummy keybinding
|
* @param tool the tool whose options will updated with a dummy keybinding
|
||||||
* @param owner the owner of the action that may be installed
|
* @param owner the owner of the action that may be installed
|
||||||
*/
|
*/
|
||||||
public static void registerDummy(PluginTool tool, String owner) {
|
public static void registerDummy(Tool tool, String owner) {
|
||||||
tool.getToolActions().registerSharedActionPlaceholder(new DeleteActionPlaceholder(owner));
|
tool.getToolActions().registerSharedActionPlaceholder(new DeleteActionPlaceholder(owner));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +84,7 @@ public class DeleteTableRowAction extends DockingAction {
|
|||||||
super(name, owner, KeyBindingType.SHARED);
|
super(name, owner, KeyBindingType.SHARED);
|
||||||
|
|
||||||
setDescription("Remove the selected rows from the table");
|
setDescription("Remove the selected rows from the table");
|
||||||
setHelpLocation(new HelpLocation(HelpTopics.SEARCH, "Remove_Items"));
|
setHelpLocation(new HelpLocation("Search", "Remove_Items"));
|
||||||
setToolBarData(new ToolBarData(ICON, null));
|
setToolBarData(new ToolBarData(ICON, null));
|
||||||
setPopupMenuData(new MenuData(new String[] { "Remove Items" }, ICON, menuGroup));
|
setPopupMenuData(new MenuData(new String[] { "Remove Items" }, ICON, menuGroup));
|
||||||
|
|
||||||
@@ -166,7 +165,7 @@ public class DeleteTableRowAction extends DockingAction {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void selectRow(TableModel model, final int row) {
|
protected void selectRow(TableModel model, final int row) {
|
||||||
Swing.runLater(() -> {
|
Swing.runLater(() -> {
|
||||||
|
|
||||||
if (checkForBusy(model)) {
|
if (checkForBusy(model)) {
|
||||||
|
Before Width: | Height: | Size: 660 B After Width: | Height: | Size: 660 B |
@@ -21,6 +21,8 @@ import java.util.List;
|
|||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import docking.widgets.search.FindDialogSearcher;
|
||||||
|
import docking.widgets.search.SearchResults;
|
||||||
import ghidra.util.Swing;
|
import ghidra.util.Swing;
|
||||||
|
|
||||||
public class FindDialogTest {
|
public class FindDialogTest {
|
||||||
@@ -43,11 +45,6 @@ public class FindDialogTest {
|
|||||||
return new CursorPosition(0);
|
return new CursorPosition(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setCursorPosition(CursorPosition position) {
|
|
||||||
// stub
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CursorPosition getStart() {
|
public CursorPosition getStart() {
|
||||||
return new CursorPosition(0);
|
return new CursorPosition(0);
|
||||||
@@ -59,20 +56,19 @@ public class FindDialogTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void highlightSearchResults(SearchLocation location) {
|
public SearchResults search(String text, CursorPosition cursorPosition,
|
||||||
// stub
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clearHighlights() {
|
|
||||||
// stub
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SearchLocation search(String text, CursorPosition cursorPosition,
|
|
||||||
boolean searchForward, boolean useRegex) {
|
boolean searchForward, boolean useRegex) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SearchResults searchAll(String text, boolean useRegex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispose() {
|
||||||
|
// stub
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,400 @@
|
|||||||
|
/* ###
|
||||||
|
* IP: GHIDRA
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package docking.widgets.search;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import javax.swing.text.Highlighter.Highlight;
|
||||||
|
import javax.swing.text.StyledDocument;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import docking.test.AbstractDockingTest;
|
||||||
|
import docking.widgets.CursorPosition;
|
||||||
|
import docking.widgets.SearchLocation;
|
||||||
|
import ghidra.util.worker.Worker;
|
||||||
|
|
||||||
|
public class TextComponentSearcherTest extends AbstractDockingTest {
|
||||||
|
|
||||||
|
private JTextPane textPane = new JTextPane();
|
||||||
|
private TextComponentSearcher searcher = new TextComponentSearcher(textPane);
|
||||||
|
|
||||||
|
private int lineCount = 0;
|
||||||
|
private List<Line> expectedMatches = new ArrayList<>();
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
|
||||||
|
JScrollPane scrollPane = new JScrollPane(textPane);
|
||||||
|
|
||||||
|
JFrame frame = new JFrame("Find Dialog Test");
|
||||||
|
frame.setSize(400, 400);
|
||||||
|
frame.getContentPane().add(scrollPane);
|
||||||
|
frame.setVisible(true);
|
||||||
|
waitForSwing();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFindNextPrevious_ChangeDocument() throws Exception {
|
||||||
|
|
||||||
|
// After changing the document, the results are stale and highlights should not work. But,
|
||||||
|
// the results should remain.
|
||||||
|
|
||||||
|
createDocumenText();
|
||||||
|
|
||||||
|
String searchText = "text";
|
||||||
|
TextComponentSearchResults results = searchNext(searchText);
|
||||||
|
assertFalse(isEmpty(results));
|
||||||
|
assertValid(results, searchText);
|
||||||
|
assertTrue(hasHighlights(results));
|
||||||
|
|
||||||
|
add("More text in the document after the search");
|
||||||
|
waitFor(results);
|
||||||
|
assertInvalid(results, searchText);
|
||||||
|
assertFalse(isEmpty(results));
|
||||||
|
assertFalse(hasHighlights(results));
|
||||||
|
|
||||||
|
// call search again and get new, valid results
|
||||||
|
TextComponentSearchResults newResults = searchNext(searchText);
|
||||||
|
assertNotEquals(results, newResults);
|
||||||
|
assertFalse(isEmpty(newResults));
|
||||||
|
assertValid(newResults, searchText);
|
||||||
|
assertTrue(hasHighlights(newResults));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearchAll_ChangeDocument() throws Exception {
|
||||||
|
|
||||||
|
// After changing the document, the results are stale and highlights should not work. But,
|
||||||
|
// the results should remain.
|
||||||
|
createDocumenText();
|
||||||
|
|
||||||
|
String searchText = "text";
|
||||||
|
TextComponentSearchResults results = searchAll(searchText);
|
||||||
|
assertFalse(isEmpty(results));
|
||||||
|
assertValid(results, searchText);
|
||||||
|
assertTrue(hasHighlights(results));
|
||||||
|
|
||||||
|
add("More text in the document after the search");
|
||||||
|
waitFor(results);
|
||||||
|
assertInvalid(results, searchText);
|
||||||
|
assertFalse(isEmpty(results));
|
||||||
|
assertFalse(hasHighlights(results));
|
||||||
|
|
||||||
|
// call search again and get new, valid results
|
||||||
|
TextComponentSearchResults newResults = searchAll(searchText);
|
||||||
|
assertNotEquals(results, newResults);
|
||||||
|
assertFalse(isEmpty(newResults));
|
||||||
|
assertValid(newResults, searchText);
|
||||||
|
assertTrue(hasHighlights(newResults));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearchAll_ContextTruncation_EvenLimit() throws Exception {
|
||||||
|
//
|
||||||
|
// Test that the context generated for the search results gets correctly truncated. We test
|
||||||
|
// various boundary conditions
|
||||||
|
//
|
||||||
|
int max = 20;
|
||||||
|
doTest(max);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearchAll_ContextTruncation_OddLimit() throws Exception {
|
||||||
|
int max = 21;
|
||||||
|
doTest(max);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearchAll_ManySearches_Activation() throws Exception {
|
||||||
|
//
|
||||||
|
// Test that we can activate and deactivate many search results rapidly and get the correct
|
||||||
|
// behavior. There is a worker queue managing activation requests. We hope to ensure the
|
||||||
|
// jobs are processed correctly.
|
||||||
|
//
|
||||||
|
createDocumenText();
|
||||||
|
|
||||||
|
// text
|
||||||
|
Map<String, SearchResults> allResults = new HashMap<>();
|
||||||
|
SearchResults results = searchAll("text");
|
||||||
|
allResults.put("text", results);
|
||||||
|
|
||||||
|
// some
|
||||||
|
results = searchAll("some");
|
||||||
|
allResults.put("some", results);
|
||||||
|
|
||||||
|
// leading
|
||||||
|
results = searchAll("leading");
|
||||||
|
allResults.put("leading", results);
|
||||||
|
|
||||||
|
// trailing
|
||||||
|
results = searchAll("trailing");
|
||||||
|
allResults.put("trailing", results);
|
||||||
|
|
||||||
|
Set<Entry<String, SearchResults>> entries = allResults.entrySet();
|
||||||
|
for (Entry<String, SearchResults> entry : entries) {
|
||||||
|
String searchText = entry.getKey();
|
||||||
|
TextComponentSearchResults searchResults =
|
||||||
|
(TextComponentSearchResults) entry.getValue();
|
||||||
|
searchResults.activate();
|
||||||
|
waitFor(searchResults);
|
||||||
|
assertTrue("Search results did not activate '%s'".formatted(searchText),
|
||||||
|
searchResults.isActive());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each call could possible cancel any activate request that has not finished. The last
|
||||||
|
// request should be active.
|
||||||
|
allResults.get("leading").activate();
|
||||||
|
allResults.get("trailing").activate();
|
||||||
|
allResults.get("leading").activate();
|
||||||
|
allResults.get("text").activate();
|
||||||
|
allResults.get("some").activate();
|
||||||
|
|
||||||
|
String searchText = "some";
|
||||||
|
TextComponentSearchResults lastResults =
|
||||||
|
(TextComponentSearchResults) allResults.get(searchText);
|
||||||
|
waitFor(lastResults);
|
||||||
|
assertTrue("Search results did not activate '%s'".formatted(searchText),
|
||||||
|
lastResults.isActive());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearchAll_ManySearches_Disposal() throws Exception {
|
||||||
|
//
|
||||||
|
// Test that we can activate and deactivate many search results rapidly and get the correct
|
||||||
|
// behavior. There is a worker queue managing activation requests. We hope to ensure the
|
||||||
|
// jobs are processed correctly.
|
||||||
|
//
|
||||||
|
createDocumenText();
|
||||||
|
|
||||||
|
TextComponentSearchResults results = searchAll("text");
|
||||||
|
assertTrue(results.isActive());
|
||||||
|
|
||||||
|
results.deactivate();
|
||||||
|
assertInactive(results);
|
||||||
|
|
||||||
|
results.activate();
|
||||||
|
assertActive(results);
|
||||||
|
|
||||||
|
results.dispose();
|
||||||
|
assertInactive(results);
|
||||||
|
|
||||||
|
// make sure active() does not work once disposed
|
||||||
|
results.activate();
|
||||||
|
assertDisposed(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextComponentSearchResults searchNext(String text) {
|
||||||
|
CursorPosition cursor = new CursorPosition(0);
|
||||||
|
SearchResults results = searcher.search(text, cursor, true, false);
|
||||||
|
waitFor(results);
|
||||||
|
return (TextComponentSearchResults) results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextComponentSearchResults searchAll(String text) {
|
||||||
|
TextComponentSearchResults results = searcher.searchAll(text, false);
|
||||||
|
waitFor(results);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void waitFor(SearchResults results) {
|
||||||
|
Worker worker = results.getWorker();
|
||||||
|
waitFor(() -> !worker.isBusy());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doTest(int max) throws Exception {
|
||||||
|
runSwing(() -> searcher.setMaxContextChars(max));
|
||||||
|
|
||||||
|
String searchText = "gold";
|
||||||
|
createDocumenText(searchText, max);
|
||||||
|
|
||||||
|
SearchResults results = searchAll(searchText);
|
||||||
|
assertMatches(results, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createDocumenText() throws Exception {
|
||||||
|
createDocumenText("stuff", 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createDocumenText(String searchText, int max) throws Exception {
|
||||||
|
add("No match on this line", false);
|
||||||
|
|
||||||
|
add("%s", searchText);
|
||||||
|
|
||||||
|
// non-truncated
|
||||||
|
add("We found %s here", searchText);
|
||||||
|
|
||||||
|
// truncated middle match
|
||||||
|
add("This text will be (%s) truncated, since it is more than max", searchText);
|
||||||
|
|
||||||
|
// truncated beginning match
|
||||||
|
add("%s, this text will be truncated, since it is more than max", searchText);
|
||||||
|
|
||||||
|
// truncated end match
|
||||||
|
add("This text will be truncated, since it is more than max, %s", searchText);
|
||||||
|
|
||||||
|
// truncated at beginning boundary
|
||||||
|
int half = (max - searchText.length()) / 2; // 'half' of the available space; max includes search text
|
||||||
|
add(padLeft(half, "%s with some trailing text that is quite long", searchText));
|
||||||
|
add(padLeft(half - 1, "%s with some trailing text that is quite long", searchText));
|
||||||
|
add(padLeft(half + 1, "%s with some trailing text that is quite long", searchText));
|
||||||
|
|
||||||
|
// truncated at end boundary
|
||||||
|
add(padRight(half, "this is some leading text that is quite long, %s", searchText));
|
||||||
|
add(padRight(half - 1, "this is some leading text that is quite long, %s", searchText));
|
||||||
|
add(padRight(half + 1, "this is some leading text that is quite long, %s", searchText));
|
||||||
|
|
||||||
|
// truncated at beginning at ellipses
|
||||||
|
add(padLeft(half, "%s with some trailing text that is quite long", searchText));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertMatches(SearchResults results, int max) {
|
||||||
|
|
||||||
|
List<SearchLocation> locations = results.getLocations();
|
||||||
|
Map<Integer, SearchLocation> locationsByLine = locations.stream()
|
||||||
|
.collect(Collectors.toMap(loc -> loc.getLineNumber(), Function.identity()));
|
||||||
|
|
||||||
|
// Debug
|
||||||
|
// Set<Entry<Integer, SearchLocation>> entries1 = locationsByLine.entrySet();
|
||||||
|
// for (Entry<Integer, SearchLocation> entry : entries1) {
|
||||||
|
// SearchLocation loc = entry.getValue();
|
||||||
|
// Msg.debug(this, loc.getContext());
|
||||||
|
// }
|
||||||
|
|
||||||
|
for (Line line : expectedMatches) {
|
||||||
|
int n = line.lineNumber();
|
||||||
|
SearchLocation result = locationsByLine.remove(n);
|
||||||
|
assertNotNull("No match at line: " + n, result);
|
||||||
|
|
||||||
|
SearchLocationContext context = result.getContext();
|
||||||
|
String text = context.getPlainText();
|
||||||
|
int length = text.length();
|
||||||
|
int maxWithEllipses = max + 6; // ... text ...
|
||||||
|
assertTrue("Length is to long. Expected max %s, but found %s"
|
||||||
|
.formatted(maxWithEllipses, length),
|
||||||
|
length <= maxWithEllipses);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!locationsByLine.isEmpty()) {
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("Found more search results than expected:").append('\n');
|
||||||
|
|
||||||
|
Set<Entry<Integer, SearchLocation>> entries = locationsByLine.entrySet();
|
||||||
|
for (Entry<Integer, SearchLocation> entry : entries) {
|
||||||
|
SearchLocation loc = entry.getValue();
|
||||||
|
sb.append(loc.toString()).append('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
fail(sb.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertValid(TextComponentSearchResults results, String searchText) {
|
||||||
|
boolean invalid = runSwing(() -> results.isInvalid(searchText));
|
||||||
|
assertFalse(invalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertInvalid(TextComponentSearchResults results, String searchText) {
|
||||||
|
boolean invalid = runSwing(() -> results.isInvalid(searchText));
|
||||||
|
assertTrue(invalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isEmpty(TextComponentSearchResults results) {
|
||||||
|
return runSwing(() -> results.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasHighlights(TextComponentSearchResults results) {
|
||||||
|
Highlight[] highlights = runSwing(() -> results.getHighlights());
|
||||||
|
return highlights.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertActive(TextComponentSearchResults results) {
|
||||||
|
waitFor(results);
|
||||||
|
assertTrue(results.isActive());
|
||||||
|
Highlight[] highlights = runSwing(() -> results.getHighlights());
|
||||||
|
List<SearchLocation> locations = results.getLocations();
|
||||||
|
assertEquals(locations.size(), highlights.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertInactive(TextComponentSearchResults results) {
|
||||||
|
waitFor(results);
|
||||||
|
assertFalse(results.isActive());
|
||||||
|
Highlight[] highlights = runSwing(() -> results.getHighlights());
|
||||||
|
assertEquals(0, highlights.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertDisposed(TextComponentSearchResults results) {
|
||||||
|
waitFor(results);
|
||||||
|
assertFalse(results.isActive());
|
||||||
|
Highlight[] highlights = runSwing(() -> results.getHighlights());
|
||||||
|
assertEquals(0, highlights.length);
|
||||||
|
List<SearchLocation> locations = results.getLocations();
|
||||||
|
assertEquals(0, locations.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String padLeft(int n, String s, String searchText) {
|
||||||
|
s = s.formatted(searchText);
|
||||||
|
String pad = StringUtils.repeat('@', n);
|
||||||
|
return pad + s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String padRight(int n, String s, String searchText) {
|
||||||
|
s = s.formatted(searchText);
|
||||||
|
String pad = StringUtils.repeat('@', n);
|
||||||
|
return s + pad;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void add(String s) throws Exception {
|
||||||
|
add(s, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void add(String raw, String searchText) throws Exception {
|
||||||
|
String s = raw;
|
||||||
|
boolean hasMatch = searchText != null;
|
||||||
|
if (hasMatch) {
|
||||||
|
s = raw.formatted(searchText);
|
||||||
|
}
|
||||||
|
add(s, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void add(String s, boolean hasMatch) throws Exception {
|
||||||
|
|
||||||
|
runSwingWithException(() -> {
|
||||||
|
StyledDocument sd = textPane.getStyledDocument();
|
||||||
|
sd.insertString(sd.getLength(), s + '\n', null);
|
||||||
|
});
|
||||||
|
|
||||||
|
lineCount++;
|
||||||
|
|
||||||
|
if (!hasMatch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Line line = new Line(s, lineCount);
|
||||||
|
expectedMatches.add(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Line(String text, int lineNumber) {}
|
||||||
|
}
|
||||||
@@ -18,6 +18,8 @@ package generic.concurrent;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import ghidra.util.task.CancelledListener;
|
import ghidra.util.task.CancelledListener;
|
||||||
import ghidra.util.task.TaskMonitor;
|
import ghidra.util.task.TaskMonitor;
|
||||||
@@ -487,29 +489,65 @@ public class ConcurrentQ<I, R> {
|
|||||||
* @return a list of all items that have not yet been queued to the threadPool.
|
* @return a list of all items that have not yet been queued to the threadPool.
|
||||||
*/
|
*/
|
||||||
public List<I> cancelAllTasks(boolean interruptRunningTasks) {
|
public List<I> cancelAllTasks(boolean interruptRunningTasks) {
|
||||||
List<FutureTaskMonitor<I, R>> tasksToBeCancelled = new ArrayList<>();
|
return cancelAllTasks(i -> true, interruptRunningTasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels the processing of currently scheduled items in this queue that match the given
|
||||||
|
* predicate. Any items that haven't yet been scheduled on the threadPool are returned
|
||||||
|
* immediately from this call. Items that are currently being processed will be cancelled and
|
||||||
|
* those results will be available on the next waitForResults() call and also if there is a
|
||||||
|
* QItemListener, it will be called with the QResult. There is no guarantee that scheduled
|
||||||
|
* tasks will terminate any time soon. If they check the isCancelled() state of their QMonitor,
|
||||||
|
* it will be true. Setting the interruptRunningTasks to true, will result in a thread
|
||||||
|
* interrupt to any currently running task which might be useful if the task perform waiting
|
||||||
|
* operations like I/O.
|
||||||
|
*
|
||||||
|
* @param p the predicate that signals which jobs to cancel
|
||||||
|
* @param interruptRunningTasks if true, an attempt will be made to interrupt any currently
|
||||||
|
* processing thread.
|
||||||
|
* @return a list of all items that have not yet been queued to the threadPool.
|
||||||
|
*/
|
||||||
|
public List<I> cancelAllTasks(Predicate<I> p, boolean interruptRunningTasks) {
|
||||||
|
List<FutureTaskMonitor<I, R>> tasksToCancel;
|
||||||
List<I> nonStartedItems;
|
List<I> nonStartedItems;
|
||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
nonStartedItems = removeUnscheduledJobs();
|
nonStartedItems = removeUnscheduledJobs(p);
|
||||||
tasksToBeCancelled.addAll(taskSet);
|
tasksToCancel = taskSet.stream()
|
||||||
|
.filter(t -> p.test(t.getItem()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
}
|
}
|
||||||
for (FutureTaskMonitor<I, R> task : tasksToBeCancelled) {
|
|
||||||
|
for (FutureTaskMonitor<I, R> task : tasksToCancel) {
|
||||||
task.cancel(interruptRunningTasks);
|
task.cancel(interruptRunningTasks);
|
||||||
}
|
}
|
||||||
return nonStartedItems;
|
return nonStartedItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all unscheduled jobs
|
||||||
|
* @return the removed jobs
|
||||||
|
*/
|
||||||
public List<I> removeUnscheduledJobs() {
|
public List<I> removeUnscheduledJobs() {
|
||||||
|
return removeUnscheduledJobs(i -> true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all unscheduled jobs matching the given predicate
|
||||||
|
* @param p the predicate
|
||||||
|
* @return the removed jobs
|
||||||
|
*/
|
||||||
|
public List<I> removeUnscheduledJobs(Predicate<I> p) {
|
||||||
List<I> nonStartedItems = new ArrayList<>();
|
List<I> nonStartedItems = new ArrayList<>();
|
||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
tracker.neverStartedItemsRemoved(queue.size());
|
nonStartedItems = queue.stream().filter(p).collect(Collectors.toList());
|
||||||
nonStartedItems.addAll(queue);
|
tracker.neverStartedItemsRemoved(nonStartedItems.size());
|
||||||
queue.clear();
|
queue.removeAll(nonStartedItems);
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
@@ -19,6 +19,7 @@ import java.util.List;
|
|||||||
import java.util.concurrent.BlockingQueue;
|
import java.util.concurrent.BlockingQueue;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
import generic.concurrent.*;
|
import generic.concurrent.*;
|
||||||
import ghidra.util.Msg;
|
import ghidra.util.Msg;
|
||||||
@@ -187,9 +188,8 @@ public abstract class AbstractWorker<T extends Job> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedules the job for execution. Jobs will be processed in priority order. The
|
* Schedules the job for execution. Jobs will be processed according to the queue supplied at
|
||||||
* highest priority jobs are those with the lowest value return by the job's getPriority()
|
* construction time (e.g., in priority order or 1 at a time).
|
||||||
* method. (i.e. the job with priority 0 will be processed before the job with priority 1)
|
|
||||||
* @param job the job to be executed.
|
* @param job the job to be executed.
|
||||||
*/
|
*/
|
||||||
public void schedule(T job) {
|
public void schedule(T job) {
|
||||||
@@ -206,7 +206,22 @@ public abstract class AbstractWorker<T extends Job> {
|
|||||||
* Clears any pending jobs and cancels any currently executing job.
|
* Clears any pending jobs and cancels any currently executing job.
|
||||||
*/
|
*/
|
||||||
public void clearAllJobs() {
|
public void clearAllJobs() {
|
||||||
clearAllJobs(false);
|
clearAllJobs(t -> true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears any pending jobs and currently executing jobs that match the given predicate.
|
||||||
|
* @param p the predicate
|
||||||
|
*/
|
||||||
|
public void clearAllJobs(Predicate<T> p) {
|
||||||
|
doClearAllJobs(p, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doClearAllJobs(Predicate<T> p, boolean interruptRunninJob) {
|
||||||
|
List<T> pendingJobs = concurrentQ.cancelAllTasks(p, interruptRunninJob);
|
||||||
|
for (T job : pendingJobs) {
|
||||||
|
job.cancel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -222,14 +237,7 @@ public abstract class AbstractWorker<T extends Job> {
|
|||||||
* </b>
|
* </b>
|
||||||
*/
|
*/
|
||||||
public void clearAllJobsWithInterrupt_IKnowTheRisks() {
|
public void clearAllJobsWithInterrupt_IKnowTheRisks() {
|
||||||
clearAllJobs(true);
|
doClearAllJobs(t -> true, true);
|
||||||
}
|
|
||||||
|
|
||||||
private void clearAllJobs(boolean interruptRuningJob) {
|
|
||||||
List<T> pendingJobs = concurrentQ.cancelAllTasks(interruptRuningJob);
|
|
||||||
for (T job : pendingJobs) {
|
|
||||||
job.cancel();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
@@ -15,14 +15,16 @@
|
|||||||
*/
|
*/
|
||||||
package ghidra.util.worker;
|
package ghidra.util.worker;
|
||||||
|
|
||||||
import ghidra.util.task.TaskMonitor;
|
|
||||||
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.concurrent.PriorityBlockingQueue;
|
import java.util.concurrent.PriorityBlockingQueue;
|
||||||
|
|
||||||
|
import ghidra.util.task.TaskMonitor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a single job at a time in priority order.
|
* Executes a single job at a time in priority order.
|
||||||
*
|
* <p>
|
||||||
|
* The highest priority jobs are those with the lowest value return by the job's getPriority()
|
||||||
|
* method. (i.e. the job with priority 0 will be processed before the job with priority 1)
|
||||||
* @see Worker
|
* @see Worker
|
||||||
*/
|
*/
|
||||||
public class PriorityWorker extends AbstractWorker<PriorityJob> {
|
public class PriorityWorker extends AbstractWorker<PriorityJob> {
|
||||||
|
|||||||
@@ -84,5 +84,4 @@ public class Worker extends AbstractWorker<Job> {
|
|||||||
super(new LinkedBlockingQueue<Job>(), isPersistentThread, name, useSharedThreadPool,
|
super(new LinkedBlockingQueue<Job>(), isPersistentThread, name, useSharedThreadPool,
|
||||||
monitor);
|
monitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ color.palette.aliceblue = rgb(45,47,65) // dark blue gray
|
|||||||
color.palette.black = darkgray
|
color.palette.black = darkgray
|
||||||
color.palette.blue = lightskyblue
|
color.palette.blue = lightskyblue
|
||||||
color.palette.blueviolet = violet
|
color.palette.blueviolet = violet
|
||||||
|
color.palette.cornflowerblue = rgb(61,91,145) // less bright cornflowerblue
|
||||||
color.palette.crimson = lightcoral
|
color.palette.crimson = lightcoral
|
||||||
color.palette.cyan = cadetblue // not sure; can change
|
color.palette.cyan = cadetblue // not sure; can change
|
||||||
color.palette.darkgray = dimgray
|
color.palette.darkgray = dimgray
|
||||||
|
|||||||
Reference in New Issue
Block a user