GP-7137 - Updated the register picker and the Register Provider to use a

natural sort
This commit is contained in:
dragonmacher 2026-08-12 13:22:31 -04:00
parent 34b446a328
commit e728c307b7
7 changed files with 488 additions and 201 deletions

View file

@ -41,6 +41,7 @@ import ghidra.program.model.listing.Program;
import ghidra.program.model.listing.ProgramContext;
import ghidra.util.Msg;
import ghidra.util.Swing;
import ghidra.util.datastruct.AlphaNumericComparator;
class VarnodeLocationCellEditor extends AbstractCellEditor
implements TableCellEditor, FocusableEditor {
@ -178,7 +179,11 @@ class VarnodeLocationCellEditor extends AbstractCellEditor
RegisterDropDownSelectionDataModel registerModel =
new RegisterDropDownSelectionDataModel(registers);
registerEntryTextField = new DropDownSelectionTextField<>(registerModel);
// A smaller min delay to make the drop-down more responsive. The number of registers
// should not be large enough to make the UI sluggish as the user is typing.
int minDelay = 200;
registerEntryTextField = new DropDownSelectionTextField<>(registerModel, minDelay);
registerEntryTextField.setBorder(null);
// this allows us to show the matching list when there is no text in the editor
@ -230,7 +235,19 @@ class VarnodeLocationCellEditor extends AbstractCellEditor
}
}
Collections.sort(registers);
Collections.sort(registers, new RegisterComparator());
return registers;
}
private static class RegisterComparator implements Comparator<Register> {
private AlphaNumericComparator alphaNumericComparator = new AlphaNumericComparator();
@Override
public int compare(Register r1, Register r2) {
String s1 = r1.getName().toLowerCase();
String s2 = r2.getName().toLowerCase();
return alphaNumericComparator.compare(s1, s2);
}
}
}

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -28,6 +28,7 @@ import docking.widgets.tree.GTreeNode;
import generic.theme.GIcon;
import ghidra.program.model.lang.Register;
import ghidra.program.model.listing.Program;
import ghidra.util.datastruct.AlphaNumericComparator;
import resources.Icons;
public class RegisterTree extends GTree {
@ -81,9 +82,8 @@ public class RegisterTree extends GTree {
list.add(reg);
}
}
Collections.sort(list);
Register[] registers = new Register[list.size()];
return list.toArray(registers);
return list.toArray(Register[]::new);
}
void setFiltered(boolean b) {
@ -111,13 +111,6 @@ public class RegisterTree extends GTree {
Register[] registers = program.getProgramContext().getRegistersWithValues();
root.setRegisters(registers);
selectRegister(currentRegister);
// TODO: old school - delete me?
// runWhenTreeIsDone(new Runnable() {
// public void run() {
// selectRegister(currentRegister);
// }
// });
}
}
@ -133,181 +126,196 @@ public class RegisterTree extends GTree {
}
return null;
}
}
abstract class SearchableRegisterTreeNode extends GTreeNode {
public GTreeNode findNode(Register register) {
List<GTreeNode> allChildren = getChildren();
for (GTreeNode child : allChildren) {
if (!(child instanceof RegisterTreeNode)) {
continue;
}
RegisterTreeNode node = (RegisterTreeNode) child;
if (node.getRegister().equals(register)) {
return node;
}
private static class RegisterNodeComparator implements Comparator<GTreeNode> {
GTreeNode foundNode = ((SearchableRegisterTreeNode) child).findNode(register);
if (foundNode != null) {
return foundNode;
private AlphaNumericComparator alphaNumericComparator =
new AlphaNumericComparator();
@Override
public int compare(GTreeNode n1, GTreeNode n2) {
String s1 = n1.getName().toLowerCase();
String s2 = n2.getName().toLowerCase();
return alphaNumericComparator.compare(s1, s2);
}
}
private static abstract class SearchableRegisterTreeNode extends GTreeNode {
private RegisterNodeComparator comparator = new RegisterNodeComparator();
void sortChildren() {
List<GTreeNode> children = new ArrayList<>(children());
Collections.sort(children, comparator);
setChildren(children);
for (GTreeNode child : children) {
SearchableRegisterTreeNode regNode = (SearchableRegisterTreeNode) child;
regNode.sortChildren();
}
}
return null;
}
}
class RegisterTreeRootNode extends SearchableRegisterTreeNode {
private Register[] lastRegisters;
@Override
public Icon getIcon(boolean expanded) {
return null;
}
@Override
public String getName() {
return "Registers";
}
@Override
public String getToolTip() {
return null;
}
@Override
public boolean isLeaf() {
return false;
}
public void setRegisters(Register[] registers) {
if (registers == lastRegisters) {
return;
}
removeAll(); // remove all current children before repopulating
lastRegisters = registers;
HashMap<String, RegisterTreeGroupNode> groups =
new HashMap<>();
List<GTreeNode> nodes = new ArrayList<>();
for (Register register : registers) {
if (register.getBaseRegister() != register &&
!register.getParentRegister().isHidden()) {
continue;
}
String groupName = register.getGroup();
if (groupName != null) {
RegisterTreeGroupNode group = groups.get(groupName);
if (group == null) {
group = new RegisterTreeGroupNode(groupName);
groups.put(groupName, group);
nodes.add(group);
public GTreeNode findNode(Register register) {
List<GTreeNode> allChildren = getChildren();
for (GTreeNode child : allChildren) {
if (!(child instanceof RegisterTreeNode)) {
continue;
}
RegisterTreeNode node = (RegisterTreeNode) child;
if (node.getRegister().equals(register)) {
return node;
}
GTreeNode foundNode = ((SearchableRegisterTreeNode) child).findNode(register);
if (foundNode != null) {
return foundNode;
}
group.addRegister(register);
}
else {
nodes.add(new RegisterTreeNode(register));
return null;
}
}
private static class RegisterTreeNode extends SearchableRegisterTreeNode {
private static Icon REG_ICON = new GIcon("icon.plugin.register");
private static Icon REG_GROUP_ICON = new GIcon("icon.plugin.register.provider");
private final Register register;
public RegisterTreeNode(Register register) {
this.register = register;
for (Register childRegister : register.getChildRegisters()) {
addNode(new RegisterTreeNode(childRegister));
}
}
Collections.sort(nodes);
setChildren(nodes);
}
}
class RegisterTreeNode extends SearchableRegisterTreeNode {
private static Icon REG_ICON = new GIcon("icon.plugin.register");
private static Icon REG_GROUP_ICON = new GIcon("icon.plugin.register.provider");
private final Register register;
public RegisterTreeNode(Register register) {
this.register = register;
for (Register childRegister : register.getChildRegisters()) {
addNode(new RegisterTreeNode(childRegister));
}
}
@Override
public Icon getIcon(boolean expanded) {
return register.hasChildren() ? REG_GROUP_ICON : REG_ICON;
}
@Override
public String getName() {
return register.getName() + " (" + register.getBitLength() + getAliases() + ")";
}
private String getAliases() {
StringBuffer buf = new StringBuffer();
for (String alias : register.getAliases()) {
buf.append(buf.length() == 0 ? "; " : ", ");
buf.append(alias);
}
return buf.toString();
}
@Override
public String getToolTip() {
return register.getDescription();
}
@Override
public boolean isLeaf() {
return !register.hasChildren();
}
@Override
public int compareTo(GTreeNode other) {
if (!(other instanceof RegisterTreeNode)) {
return 1;
}
return getName().compareTo(other.getName());
}
public Register getRegister() {
return register;
}
}
class RegisterTreeGroupNode extends SearchableRegisterTreeNode {
private static Icon OPEN_ICON = Icons.OPEN_FOLDER_ICON;
private static Icon CLOSED_ICON = Icons.CLOSED_FOLDER_ICON;
private String name;
public RegisterTreeGroupNode(String name) {
this.name = name;
}
@Override
public Icon getIcon(boolean expanded) {
return expanded ? OPEN_ICON : CLOSED_ICON;
}
@Override
public String getName() {
return name;
}
@Override
public String getToolTip() {
return null;
}
@Override
public boolean isLeaf() {
return false;
}
public void addRegister(Register register) {
addNode(new RegisterTreeNode(register));
}
@Override
public int compareTo(GTreeNode o) {
if (!(o instanceof RegisterTreeGroupNode)) {
return -1;
}
return name.compareTo(o.getName());
@Override
public Icon getIcon(boolean expanded) {
return register.hasChildren() ? REG_GROUP_ICON : REG_ICON;
}
@Override
public String getName() {
return register.getName() + " (" + register.getBitLength() + getAliases() +
")";
}
private String getAliases() {
StringBuffer buf = new StringBuffer();
for (String alias : register.getAliases()) {
buf.append(buf.length() == 0 ? "; " : ", ");
buf.append(alias);
}
return buf.toString();
}
@Override
public String getToolTip() {
return register.getDescription();
}
@Override
public boolean isLeaf() {
return !register.hasChildren();
}
Register getRegister() {
return register;
}
}
private static class RegisterTreeGroupNode extends SearchableRegisterTreeNode {
private static Icon OPEN_ICON = Icons.OPEN_FOLDER_ICON;
private static Icon CLOSED_ICON = Icons.CLOSED_FOLDER_ICON;
private String name;
public RegisterTreeGroupNode(String name) {
this.name = name;
}
@Override
public Icon getIcon(boolean expanded) {
return expanded ? OPEN_ICON : CLOSED_ICON;
}
@Override
public String getName() {
return name;
}
@Override
public String getToolTip() {
return null;
}
@Override
public boolean isLeaf() {
return false;
}
public void addRegister(Register register) {
addNode(new RegisterTreeNode(register));
}
}
private static class RegisterTreeRootNode extends SearchableRegisterTreeNode {
private Register[] lastRegisters;
@Override
public Icon getIcon(boolean expanded) {
return null;
}
@Override
public String getName() {
return "Registers";
}
@Override
public String getToolTip() {
return null;
}
@Override
public boolean isLeaf() {
return false;
}
public void setRegisters(Register[] registers) {
if (registers == lastRegisters) {
return;
}
removeAll(); // remove all current children before re-populating
lastRegisters = registers;
HashMap<String, RegisterTreeGroupNode> groups = new HashMap<>();
List<GTreeNode> nodes = new ArrayList<>();
for (Register register : registers) {
if (register.getBaseRegister() != register &&
!register.getParentRegister().isHidden()) {
continue;
}
String groupName = register.getGroup();
if (groupName != null) {
RegisterTreeGroupNode group = groups.get(groupName);
if (group == null) {
group = new RegisterTreeGroupNode(groupName);
groups.put(groupName, group);
nodes.add(group);
}
group.addRegister(register);
}
else {
nodes.add(new RegisterTreeNode(register));
}
}
setChildren(nodes);
sortChildren();
}
}
}

View file

@ -15,15 +15,18 @@
*/
package ghidra.app.plugin.core.function.editor;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.*;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import generic.test.AbstractGenericTest;
import ghidra.program.database.ProgramBuilder;
import ghidra.program.database.ProgramDB;
import ghidra.program.model.lang.Register;
import ghidra.program.model.listing.ProgramContext;
public class VarnodeLocationCellEditorTest extends AbstractGenericTest {
private static final int AARCH64_GENERAL_REGISTER_MAX = 30;
@ -31,24 +34,23 @@ public class VarnodeLocationCellEditorTest extends AbstractGenericTest {
@Test
public void testAarch64XRegistersUseNumericOrder() throws Exception {
ProgramBuilder builder = new ProgramBuilder("TestProgram", ProgramBuilder._AARCH64);
try {
List<Register> registers = VarnodeLocationCellEditor.getSortedVisibleRegisters(
builder.getProgram().getProgramContext());
List<String> xRegisters = new ArrayList<>();
for (Register register : registers) {
if (register.getName().matches("x\\d+")) {
xRegisters.add(register.getName());
}
}
ProgramDB p = builder.getProgram();
builder.dispose();
List<String> expected = new ArrayList<>();
for (int i = 0; i <= AARCH64_GENERAL_REGISTER_MAX; i++) {
expected.add("x" + i);
ProgramContext context = p.getProgramContext();
List<Register> registers = VarnodeLocationCellEditor.getSortedVisibleRegisters(context);
List<String> xRegisters = new ArrayList<>();
for (Register register : registers) {
if (register.getName().matches("x\\d+")) {
xRegisters.add(register.getName());
}
assertEquals(expected, xRegisters);
}
finally {
builder.dispose();
List<String> expected = new ArrayList<>();
for (int i = 0; i <= AARCH64_GENERAL_REGISTER_MAX; i++) {
expected.add("x" + i);
}
assertEquals(expected, xRegisters);
}
}

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -48,6 +48,21 @@ public class DropDownSelectionTextField<T> extends DropDownTextField<T> {
super(dataModel);
}
/**
* This constructor allows you to control the delay between keystrokes before the items in the
* view get updated. A reasonable delay allows users to type multiple characters before the UI
* updates. For large data, this prevents the UI from feeling sluggish, since the UI is does
* not have to process each keystroke. For small data, the UI can update in real time, which
* feels responsive. The tradeoff is between feeling the most responsive vs adding a small
* delay to prevent apparent freezing. The default value is #DEFAULT_MIN_UPDATE_DELAY.
*
* @param dataModel the data model
* @param updateMinDelay the min update delay
*/
public DropDownSelectionTextField(DropDownTextFieldDataModel<T> dataModel, int updateMinDelay) {
super(dataModel, updateMinDelay);
}
@Override
protected ListSelectionModel createListSelectionModel() {
DefaultListSelectionModel model = new DefaultListSelectionModel();

View file

@ -72,6 +72,7 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
private static final Cursor CURSOR_HAND = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
private static final Cursor CURSOR_DEFAULT = Cursor.getDefaultCursor();
protected static final int DEFAULT_MIN_UPDATE_DELAY = 350;
private static final int DEFAULT_MAX_UPDATE_DELAY = 2000;
private static final int MIN_HEIGHT = 300;
private static final int MIN_WIDTH = 200;
@ -136,7 +137,7 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
* @param dataModel provides element storage and search capabilities to this component.
*/
public DropDownTextField(DropDownTextFieldDataModel<T> dataModel) {
this(dataModel, 350);
this(dataModel, DEFAULT_MIN_UPDATE_DELAY);
}
/**

View file

@ -0,0 +1,160 @@
/* ###
* 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.util.datastruct;
import java.util.Comparator;
/**
* A comparator to perform a natural sort for text and numbers. Numbers in the text will compared
* based on their parsed value, not their ASCII value.
*/
public class AlphaNumericComparator implements Comparator<String> {
@Override
public int compare(String s1, String s2) {
if (s1 == null || s2 == null) {
return s1 == s2 ? 0 : (s1 == null ? -1 : 1);
}
int i = 0;
int j = 0;
int len1 = s1.length();
int len2 = s2.length();
// Core Pass: Compare semantic characters and mathematical numbers
while (i < len1 && j < len2) {
char c1 = s1.charAt(i);
char c2 = s2.charAt(j);
// Skip spaces dynamically
if (c1 == ' ') {
i++;
continue;
}
if (c2 == ' ') {
j++;
continue;
}
boolean isDigit1 = Character.isDigit(c1);
boolean isDigit2 = Character.isDigit(c2);
if (isDigit1 && isDigit2) {
// Parse full numbers from the current position
long val1 = parseNumber(s1, i);
long val2 = parseNumber(s2, j);
if (val1 != val2) {
return Long.compare(val1, val2);
}
// Skip ahead past the digit sequences we just parsed
i = skipDigits(s1, i);
j = skipDigits(s2, j);
}
else {
// Compare standard characters alphabetically
if (c1 != c2) {
return Character.compare(c1, c2);
}
i++;
j++;
}
}
// Clean up any remaining trailing spaces
i = skipSpaces(s1, i);
j = skipSpaces(s2, j);
// Tie-breaker 1: Shorter structure wins (e.g., "file4" before "file 4")
boolean ended1 = (i == len1);
boolean ended2 = (j == len2);
if (ended1 != ended2) {
return ended1 ? -1 : 1;
}
// Both strings have the same length. If they have numbers, the numbers have been
// parsed to be the same numeric value, but they may have leading zeros.
// Prefer fewer leading zeros (e.g., "file4" before "file04").
// A non-zero value means that both strings have digits that were compared.
int lengthCompare = compareDigitLengths(s1, s2);
if (lengthCompare != 0) {
return lengthCompare;
}
// Default to a string compare
return s1.compareTo(s2);
}
private long parseNumber(String s, int i) {
long value = 0;
while (i < s.length()) {
char c = s.charAt(i);
if (!Character.isDigit(c)) {
return value;
}
int intValue = (c - '0');
value = value * 10 + intValue;
i++;
}
return value;
}
private int skipDigits(String s, int i) {
int len = s.length();
while (i < len && Character.isDigit(s.charAt(i))) {
i++;
}
return i;
}
private int skipSpaces(String s, int i) {
int len = s.length();
while (i < len && s.charAt(i) == ' ') {
i++;
}
return i;
}
private int compareDigitLengths(String s1, String s2) {
int len1 = s1.length();
int len2 = s2.length();
int i = 0;
int j = 0;
while (i < len1 && j < len2) {
i = skipSpaces(s1, i);
j = skipSpaces(s2, j);
if (i >= len1 || j >= len2) {
break;
}
if (Character.isDigit(s1.charAt(i)) && Character.isDigit(s2.charAt(j))) {
int skip1 = skipDigits(s1, i) - i;
int skip2 = skipDigits(s2, j) - j;
if (skip1 != skip2) {
return Integer.compare(skip1, skip2);
}
i += skip1;
j += skip2;
}
else {
i++;
j++;
}
}
return 0;
}
}

View file

@ -0,0 +1,84 @@
/* ###
* 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.util.datastruct;
import static org.junit.Assert.*;
import java.util.*;
import org.junit.Test;
import generic.test.AbstractGenericTest;
public class AlphaNumericComparatorTest extends AbstractGenericTest {
@Test
public void testIt() {
List<String> unsorted = Arrays.asList(
" file10", // Leading spaces
"file02 ", // Trailing spaces
"file 2", // Internal spaces
"file1", // Normal
" file002 ", // Leading and trailing spaces
"file 01", // Internal space before digit
"file20",
" apple", // Leading spaces
"Apple ", // Trailing spaces
"ba nana", // Internal spaces
"a",
"alpha ", // Trailing spaces
" alphabet", // Leading spaces
"app",
"ap ple", // Internal spaces
"0.1.0",
"0.1.9",
"1.0",
"0.2.1",
"2.1.0");
List<String> sorted = Arrays.asList(
"0.1.0",
"0.1.9",
"0.2.1",
"1.0",
"2.1.0",
"Apple ",
"a",
"alpha ",
" alphabet",
"app",
" apple",
"ap ple",
"ba nana",
"file1",
"file 01",
"file 2",
"file02 ",
" file002 ",
" file10",
"file20");
Collections.shuffle(unsorted);
List<String> copy = new ArrayList<>(unsorted);
Collections.sort(copy, new AlphaNumericComparator());
assertEquals(sorted, copy);
}
}