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.program.model.listing.ProgramContext;
import ghidra.util.Msg; import ghidra.util.Msg;
import ghidra.util.Swing; import ghidra.util.Swing;
import ghidra.util.datastruct.AlphaNumericComparator;
class VarnodeLocationCellEditor extends AbstractCellEditor class VarnodeLocationCellEditor extends AbstractCellEditor
implements TableCellEditor, FocusableEditor { implements TableCellEditor, FocusableEditor {
@@ -178,7 +179,11 @@ class VarnodeLocationCellEditor extends AbstractCellEditor
RegisterDropDownSelectionDataModel registerModel = RegisterDropDownSelectionDataModel registerModel =
new RegisterDropDownSelectionDataModel(registers); 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); registerEntryTextField.setBorder(null);
// this allows us to show the matching list when there is no text in the editor // 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; 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"); * 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.
@@ -28,6 +28,7 @@ import docking.widgets.tree.GTreeNode;
import generic.theme.GIcon; import generic.theme.GIcon;
import ghidra.program.model.lang.Register; import ghidra.program.model.lang.Register;
import ghidra.program.model.listing.Program; import ghidra.program.model.listing.Program;
import ghidra.util.datastruct.AlphaNumericComparator;
import resources.Icons; import resources.Icons;
public class RegisterTree extends GTree { public class RegisterTree extends GTree {
@@ -81,9 +82,8 @@ public class RegisterTree extends GTree {
list.add(reg); list.add(reg);
} }
} }
Collections.sort(list);
Register[] registers = new Register[list.size()]; return list.toArray(Register[]::new);
return list.toArray(registers);
} }
void setFiltered(boolean b) { void setFiltered(boolean b) {
@@ -111,13 +111,6 @@ public class RegisterTree extends GTree {
Register[] registers = program.getProgramContext().getRegistersWithValues(); Register[] registers = program.getProgramContext().getRegistersWithValues();
root.setRegisters(registers); root.setRegisters(registers);
selectRegister(currentRegister); 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; return null;
} }
}
abstract class SearchableRegisterTreeNode extends GTreeNode { private static class RegisterNodeComparator implements Comparator<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;
}
GTreeNode foundNode = ((SearchableRegisterTreeNode) child).findNode(register); private AlphaNumericComparator alphaNumericComparator =
if (foundNode != null) { new AlphaNumericComparator();
return foundNode;
@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; public GTreeNode findNode(Register register) {
} List<GTreeNode> allChildren = getChildren();
} for (GTreeNode child : allChildren) {
if (!(child instanceof RegisterTreeNode)) {
class RegisterTreeRootNode extends SearchableRegisterTreeNode { continue;
private Register[] lastRegisters; }
RegisterTreeNode node = (RegisterTreeNode) child;
@Override if (node.getRegister().equals(register)) {
public Icon getIcon(boolean expanded) { return node;
return null; }
}
GTreeNode foundNode = ((SearchableRegisterTreeNode) child).findNode(register);
@Override if (foundNode != null) {
public String getName() { return foundNode;
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);
} }
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); @Override
} public Icon getIcon(boolean expanded) {
} return register.hasChildren() ? REG_GROUP_ICON : REG_ICON;
}
class RegisterTreeNode extends SearchableRegisterTreeNode {
private static Icon REG_ICON = new GIcon("icon.plugin.register"); @Override
private static Icon REG_GROUP_ICON = new GIcon("icon.plugin.register.provider"); public String getName() {
private final Register register; return register.getName() + " (" + register.getBitLength() + getAliases() +
")";
public RegisterTreeNode(Register register) { }
this.register = register;
for (Register childRegister : register.getChildRegisters()) { private String getAliases() {
addNode(new RegisterTreeNode(childRegister)); StringBuffer buf = new StringBuffer();
} for (String alias : register.getAliases()) {
} buf.append(buf.length() == 0 ? "; " : ", ");
buf.append(alias);
@Override }
public Icon getIcon(boolean expanded) { return buf.toString();
return register.hasChildren() ? REG_GROUP_ICON : REG_ICON; }
}
@Override
@Override public String getToolTip() {
public String getName() { return register.getDescription();
return register.getName() + " (" + register.getBitLength() + getAliases() + ")"; }
}
@Override
private String getAliases() { public boolean isLeaf() {
StringBuffer buf = new StringBuffer(); return !register.hasChildren();
for (String alias : register.getAliases()) { }
buf.append(buf.length() == 0 ? "; " : ", ");
buf.append(alias); Register getRegister() {
} return register;
return buf.toString(); }
} }
@Override private static class RegisterTreeGroupNode extends SearchableRegisterTreeNode {
public String getToolTip() { private static Icon OPEN_ICON = Icons.OPEN_FOLDER_ICON;
return register.getDescription(); private static Icon CLOSED_ICON = Icons.CLOSED_FOLDER_ICON;
} private String name;
@Override public RegisterTreeGroupNode(String name) {
public boolean isLeaf() { this.name = name;
return !register.hasChildren(); }
}
@Override
@Override public Icon getIcon(boolean expanded) {
public int compareTo(GTreeNode other) { return expanded ? OPEN_ICON : CLOSED_ICON;
if (!(other instanceof RegisterTreeNode)) { }
return 1;
} @Override
return getName().compareTo(other.getName()); public String getName() {
} return name;
}
public Register getRegister() {
return register; @Override
} public String getToolTip() {
} return null;
}
class RegisterTreeGroupNode extends SearchableRegisterTreeNode {
private static Icon OPEN_ICON = Icons.OPEN_FOLDER_ICON; @Override
private static Icon CLOSED_ICON = Icons.CLOSED_FOLDER_ICON; public boolean isLeaf() {
private String name; return false;
}
public RegisterTreeGroupNode(String name) {
this.name = name; public void addRegister(Register register) {
} addNode(new RegisterTreeNode(register));
}
@Override }
public Icon getIcon(boolean expanded) {
return expanded ? OPEN_ICON : CLOSED_ICON; private static class RegisterTreeRootNode extends SearchableRegisterTreeNode {
} private Register[] lastRegisters;
@Override @Override
public String getName() { public Icon getIcon(boolean expanded) {
return name; return null;
} }
@Override @Override
public String getToolTip() { public String getName() {
return null; return "Registers";
} }
@Override @Override
public boolean isLeaf() { public String getToolTip() {
return false; return null;
} }
public void addRegister(Register register) { @Override
addNode(new RegisterTreeNode(register)); public boolean isLeaf() {
} return false;
}
@Override
public int compareTo(GTreeNode o) { public void setRegisters(Register[] registers) {
if (!(o instanceof RegisterTreeGroupNode)) { if (registers == lastRegisters) {
return -1; return;
} }
return name.compareTo(o.getName());
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; 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 org.junit.Test;
import generic.test.AbstractGenericTest; import generic.test.AbstractGenericTest;
import ghidra.program.database.ProgramBuilder; import ghidra.program.database.ProgramBuilder;
import ghidra.program.database.ProgramDB;
import ghidra.program.model.lang.Register; import ghidra.program.model.lang.Register;
import ghidra.program.model.listing.ProgramContext;
public class VarnodeLocationCellEditorTest extends AbstractGenericTest { public class VarnodeLocationCellEditorTest extends AbstractGenericTest {
private static final int AARCH64_GENERAL_REGISTER_MAX = 30; private static final int AARCH64_GENERAL_REGISTER_MAX = 30;
@@ -31,24 +34,23 @@ public class VarnodeLocationCellEditorTest extends AbstractGenericTest {
@Test @Test
public void testAarch64XRegistersUseNumericOrder() throws Exception { public void testAarch64XRegistersUseNumericOrder() throws Exception {
ProgramBuilder builder = new ProgramBuilder("TestProgram", ProgramBuilder._AARCH64); ProgramBuilder builder = new ProgramBuilder("TestProgram", ProgramBuilder._AARCH64);
try { ProgramDB p = builder.getProgram();
List<Register> registers = VarnodeLocationCellEditor.getSortedVisibleRegisters( builder.dispose();
builder.getProgram().getProgramContext());
List<String> xRegisters = new ArrayList<>();
for (Register register : registers) {
if (register.getName().matches("x\\d+")) {
xRegisters.add(register.getName());
}
}
List<String> expected = new ArrayList<>(); ProgramContext context = p.getProgramContext();
for (int i = 0; i <= AARCH64_GENERAL_REGISTER_MAX; i++) { List<Register> registers = VarnodeLocationCellEditor.getSortedVisibleRegisters(context);
expected.add("x" + i);
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"); * 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.
@@ -48,6 +48,21 @@ public class DropDownSelectionTextField<T> extends DropDownTextField<T> {
super(dataModel); 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 @Override
protected ListSelectionModel createListSelectionModel() { protected ListSelectionModel createListSelectionModel() {
DefaultListSelectionModel model = new DefaultListSelectionModel(); 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_HAND = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
private static final Cursor CURSOR_DEFAULT = Cursor.getDefaultCursor(); 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 DEFAULT_MAX_UPDATE_DELAY = 2000;
private static final int MIN_HEIGHT = 300; private static final int MIN_HEIGHT = 300;
private static final int MIN_WIDTH = 200; 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. * @param dataModel provides element storage and search capabilities to this component.
*/ */
public DropDownTextField(DropDownTextFieldDataModel<T> dataModel) { 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);
}
}