Merge remote-tracking branch 'origin/GP-2-dragonmacher-rt-v6+--SQUASHED'

This commit is contained in:
Ryan Kurtz
2026-02-23 10:32:37 -05:00
13 changed files with 455 additions and 49 deletions
@@ -401,7 +401,6 @@ public abstract class AbstractFunctionGraphTest extends AbstractGhidraHeadedInte
@After
public void tearDown() throws Exception {
waitForSwing();
env.closeTool(tool);
env.dispose();
}
@@ -216,7 +216,10 @@ public class GlobalMenuAndToolBarManager implements DockingWindowListener {
// has not yet completed
DialogComponentProvider provider = dialog.getDialogComponent();
if (provider != null) {
return provider.getActionContext(null);
ActionContext context = provider.getActionContext(null);
if (context != null) {
return context;
}
}
}
@@ -225,7 +228,7 @@ public class GlobalMenuAndToolBarManager implements DockingWindowListener {
private ActionContext getComponentProviderContext(WindowNode windowNode) {
if (windowNode == null) {
return null;
return new DefaultActionContext();
}
ActionContext context = null;
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -17,6 +17,8 @@ package docking.widgets.fieldpanel.support;
import java.awt.Color;
import generic.json.Json;
public class Highlight {
private int start;
private int end;
@@ -36,21 +38,28 @@ public class Highlight {
}
/**
* Returns the starting position of the highlight.
* {@return the starting position of the highlight}
*/
public int getStart() {
return start + offset;
}
/**
* Returns the ending position (inclusive) of the highlight.
* {@return the ending position (inclusive) of the highlight}
*/
public int getEnd() {
return end + offset;
}
/**
* Returns the color to use as the background highlight color.
* {@return the number of characters in the match.}
*/
public int length() {
return (end - start) + 1; // +1 because 'end' is inclusive
}
/**
* {@return the color to use as the background highlight color.}
*/
public Color getColor() {
return color;
@@ -59,11 +68,20 @@ public class Highlight {
/**
* Sets the offset of this highlights start and end values. The effect of the offset is that
* calls to {@link #getStart()} and {@link #getEnd()} will return their values with the
* offset added.
* offset added.
* <p>
* This useful when highlights are using offsets for widgets that embedded inside of composite
* containers. the parent container turn these relative values into absolute values that work
* when all sub-parts are combined.
*
* @param newOffset The new offset into this highlight.
*/
public void setOffset(int newOffset) {
offset = newOffset;
}
@Override
public String toString() {
return Json.toString(this);
}
}
@@ -16,6 +16,7 @@
package docking.widgets.search;
import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.net.URL;
import java.util.*;
import java.util.List;
@@ -307,7 +308,11 @@ public class TextComponentSearchResults extends SearchResults {
try {
int start = location.getStartIndexInclusive();
int end = location.getEndIndexInclusive();
Rectangle startR = editorPane.modelToView2D(start).getBounds();
Rectangle2D start2d = editorPane.modelToView2D(start);
if (start2d == null) {
return; // not yet realized
}
Rectangle startR = start2d.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);
@@ -449,7 +454,7 @@ public class TextComponentSearchResults extends SearchResults {
}
Color c = location.isActive() ? activeHighlightColor : highlightColor;
HighlightPainter painter = new DefaultHighlightPainter(c);
HighlightPainter painter = new SearchResultsHighlightPainter(c);
int start = location.getStartIndexInclusive();
int end = location.getEndIndexInclusive() + 1; // +1 to make inclusive be exclusive
try {
@@ -463,6 +468,7 @@ public class TextComponentSearchResults extends SearchResults {
@Override
public void dispose() {
caretUpdater.dispose();
if (editorPane != null) {
@@ -557,7 +563,11 @@ public class TextComponentSearchResults extends SearchResults {
}
if (nonSearchDelegate) {
// Calling setHighlighter() will cause all old highlights to get removed. We would
// like the non-search highlights to remain. Grab them now and put them back after.
Highlight[] nonSearchHighlights = delegate.getHighlights();
editorPane.setHighlighter(delegate);
restoreNonSearchHighlights(nonSearchHighlights);
}
else {
editorPane.setHighlighter(null);
@@ -566,7 +576,30 @@ public class TextComponentSearchResults extends SearchResults {
@Override
public void install(JTextComponent c) {
Highlight[] nonSearchHighlights = delegate.getHighlights();
delegate.install(c);
// Calling delegate.install() will cause its existing highlights to get removed. We
// would like to keep them, so we have save and restore them.
restoreNonSearchHighlights(nonSearchHighlights);
}
private void restoreNonSearchHighlights(Highlight[] oldHighlights) {
for (Highlight hl : oldHighlights) {
int start = hl.getStartOffset();
int end = hl.getEndOffset();
HighlightPainter painter = hl.getPainter();
if (painter instanceof SearchResultsHighlightPainter) {
continue;
}
try {
delegate.addHighlight(start, end, painter);
}
catch (BadLocationException e) {
// shouldn't happen
Msg.error(this, "Could not add existing highlight", e);
}
}
}
@Override
@@ -599,7 +632,11 @@ public class TextComponentSearchResults extends SearchResults {
@Override
public void removeAllHighlights() {
delegate.removeAllHighlights();
// only remove our highlights, not any pre-existing client non-search highlights
for (TextComponentSearchLocation loc : searchLocations) {
Object tag = loc.getHighlightTag();
removeHighlight(tag);
}
}
@Override
@@ -611,6 +648,12 @@ public class TextComponentSearchResults extends SearchResults {
public Highlight[] getHighlights() {
return delegate.getHighlights();
}
}
// marker class
private class SearchResultsHighlightPainter extends DefaultHighlightPainter {
public SearchResultsHighlightPainter(Color c) {
super(c);
}
}
}
@@ -23,6 +23,8 @@ import javax.swing.JEditorPane;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import org.apache.commons.lang3.StringUtils;
import docking.widgets.CursorPosition;
import docking.widgets.SearchLocation;
import ghidra.util.Msg;
@@ -235,6 +237,11 @@ public class TextComponentSearcher implements FindDialogSearcher {
return;
}
if (StringUtils.isBlank(fullText)) {
Msg.error(this, "Cannot search a blank document");
return;
}
TreeMap<Integer, Line> lineRangeMap = mapLines(fullText);
Pattern pattern = createSearchPattern(searchText, useRegex);
@@ -252,7 +259,6 @@ public class TextComponentSearcher implements FindDialogSearcher {
context);
matchesByPosition.put(start, location);
}
}
private TreeMap<Integer, Line> mapLines(String fullText) {
@@ -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.
@@ -62,10 +62,14 @@ public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
protected ServiceProvider serviceProvider;
private TableColumnDescriptor<ROW_TYPE> columnDescriptor;
protected List<DynamicTableColumn<ROW_TYPE, ?, ?>> tableColumns = new ArrayList<>();
private List<DynamicTableColumn<ROW_TYPE, ?, ?>> defaultTableColumns = new ArrayList<>();
protected Map<DynamicTableColumn<ROW_TYPE, ?, ?>, Settings> columnSettings = new HashMap<>();
/** All currently visible columns */
protected List<DynamicTableColumn<ROW_TYPE, ?, ?>> tableColumns = new ArrayList<>();
/** The initially visible columns before user changes or state restoring */
private List<DynamicTableColumn<ROW_TYPE, ?, ?>> defaultTableColumns = new ArrayList<>();
private boolean ignoreSettingChanges = false;
public GDynamicColumnTableModel(ServiceProvider serviceProvider) {
@@ -298,7 +302,7 @@ public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
protected void addTableColumns(Set<DynamicTableColumn<ROW_TYPE, ?, ?>> columns,
boolean isDefault) {
for (DynamicTableColumn<ROW_TYPE, ?, ?> column : columns) {
doAddTableColumn(column, getDefaultTableColumns().size(), isDefault);
doAddTableColumn(column, -1, isDefault);
}
fireTableStructureChanged();
}
@@ -327,16 +331,30 @@ public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
private void doAddTableColumn(DynamicTableColumn<ROW_TYPE, ?, ?> column, int index,
boolean isDefault) {
if (index < 0 || index > tableColumns.size()) {
index = getDefaultTableColumns().size();
int adjustedIndex = index;
if (adjustedIndex < 0 || adjustedIndex > tableColumns.size()) {
adjustedIndex = tableColumns.size();
}
tableColumns.add(index, column);
tableColumns.add(adjustedIndex, column);
columnSettings.put(column, new SettingsImpl(this, column));
if (isDefault) {
List<DynamicTableColumn<ROW_TYPE, ?, ?>> defaultColumns = getDefaultTableColumns();
defaultColumns.add(index, column);
if (!isDefault) {
return;
}
// Note: this method is typically called when 'tableColumns' and 'defaultTableColumns' have
// the same columns. But, that is not a requirement. When they have the same columns, the
// insertion index is correct for both lists. If they have different columns, then the
// insertion index for the default columns may or may not be what the caller intended. In
// practice, it should not matter where the column is inserted into the default columns, as
// that is only used to query whether a column is in the list or not. If we ever need to
// have accurate positioning in the default list when both lists are not equivalent, then we
// will have to add a new method or change this method to allow callers to dictate where the
// column should go in the default list. For now, just add the column to the end.
adjustedIndex = defaultTableColumns.size();
List<DynamicTableColumn<ROW_TYPE, ?, ?>> defaultColumns = getDefaultTableColumns();
defaultColumns.add(adjustedIndex, column);
}
/**
@@ -470,7 +488,7 @@ public abstract class GDynamicColumnTableModel<ROW_TYPE, DATA_SOURCE>
DATA_SOURCE dataSource = getDataSource();
@SuppressWarnings("unchecked")
// TODO: We are casting now, as in practice the type should never be different that
// Note: We are casting now, as in practice the type should never be different that
// the declared type. We want to remove entirely the 'dataSource' value and then
// the templating will be simpler.
DynamicTableColumn<ROW_TYPE, ?, DATA_SOURCE> column =
@@ -470,29 +470,30 @@ public class TableColumnModelState implements SortListener {
if (preferenceKey != null) {
return preferenceKey;
}
TableModel tableModel = table.getModel();
int columnCount = getDefaultColumnCount();
StringBuffer buffer = new StringBuffer();
TableModel model = table.getModel();
int n = model.getColumnCount();
StringBuilder buffer = new StringBuilder();
buffer.append(getTableModelName());
buffer.append(":");
for (int i = 0; i < columnCount; i++) {
String columnName = tableModel.getColumnName(i);
buffer.append(columnName).append(":");
for (int i = 0; i < n; i++) {
if (isDefaultColumn(i)) {
String columnName = model.getColumnName(i);
buffer.append(columnName).append(":");
}
}
return buffer.toString();
}
private int getDefaultColumnCount() {
private boolean isDefaultColumn(int col) {
TableModel tableModel = table.getUnwrappedTableModel();
if (tableModel instanceof VariableColumnTableModel) {
VariableColumnTableModel variableTableModel = (VariableColumnTableModel) tableModel;
// VariableColumnTableModels have default columns and 'found' columns. We only want to
// create a key based upon the default columns
return variableTableModel.getDefaultColumnCount();
return variableTableModel.isDefaultColumn(col);
}
return tableModel.getColumnCount();
return true;
}
private void setDefaultColumnsVisible() {
@@ -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.
@@ -27,6 +27,7 @@ public interface VariableColumnTableModel extends TableModel {
* type or is wraps another table model that is an instance of this type. If the given
* model is not such an instance, then null is returned.
*
* @param m the model
* @return the variable column model
*/
public static VariableColumnTableModel from(TableModel m) {
@@ -45,6 +46,7 @@ public interface VariableColumnTableModel extends TableModel {
* Returns a value that is unique for a given table column. This is different than getting
* the display name, which may be shared by different columns.
* @param column the index (in the model space) of the column for which to get the identifier
* @return the identifier
*/
public String getUniqueIdentifier(int column);
@@ -56,7 +58,9 @@ public interface VariableColumnTableModel extends TableModel {
* calling methods like {@link #getColumnName(int)}.
*
* @return Gets the count of the default columns for this model.
* @deprecated no longer needed
*/
@Deprecated(forRemoval = true, since = "12.1")
public int getDefaultColumnCount();
/**
@@ -0,0 +1,41 @@
/* ###
* 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 generic.algorithms;
/**
* A reducing LCS that works on Strings.
*/
public class StringReducingLcs extends ReducingLcs<String, Character> {
public StringReducingLcs(String x, String y) {
super(x, y);
}
@Override
protected String reduce(String input, int start, int end) {
return input.substring(start, end);
}
@Override
protected int lengthOf(String s) {
return s.length();
}
@Override
protected Character valueOf(String s, int offset) {
return s.charAt(offset);
}
}
@@ -0,0 +1,270 @@
/* ###
* 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 generic.algorithms;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;
/**
* Finds differences between two words (any two Strings). The results are available via
* {@link #getParts()}.
*/
public class WordDiffer {
private List<WordPart> parts = List.of();
/**
* Diffs the text between the old and new word. The new word is the current version of two
* Strings, the old word is the previous version.
*
* @param oldWord the previous version of the text
* @param newWord the current version of the text
*/
public WordDiffer(String oldWord, String newWord) {
LcsMatch lcs = getLcs(newWord, oldWord);
if (lcs == null) {
return;
}
TreeMap<Integer, String> wordsByOffset = buildWordOffsets(lcs);
parts = createWordParts(newWord, wordsByOffset);
}
/**
* Returns the 'new word' broken into Strings with offsets, with each part being the same text
* or different text.
* @return the parts; empty if the LCS could not be created
*/
public List<WordPart> getParts() {
return parts;
}
/**
* The same as {@link #getParts()} except that this method will merge differences that are
* separated only by {@code maxSize} or less characters. This method allows clients to combine
* many smaller differences into larger differences that span similar characters. Merging parts
* can reduce visual clutter when displaying the differences, at the expense of accuracy.
*
* @param maxSize the maximum span of characters past which not to merge two differences
* @return the parts
*/
public List<WordPart> getMergedParts(int maxSize) {
List<WordPart> newParts = new ArrayList<>();
DifferentPart lastDiffPart = null;
for (int i = 0; i < parts.size(); i++) {
WordPart part = parts.get(i);
if (part instanceof DifferentPart diffPart) {
lastDiffPart = diffPart;
newParts.add(lastDiffPart);
continue;
}
int length = part.length();
if (length > maxSize) {
continue;
}
if (lastDiffPart != null) {
WordPart nextPart = i + 1 < parts.size() ? parts.get(i + 1) : null;
WordPart mergedPart = lastDiffPart.merge(part, nextPart);
if (mergedPart != null) {
i++;
newParts.remove(lastDiffPart);
newParts.add(mergedPart);
lastDiffPart = null;
}
}
// add this non-diff part to the last merged diff part, if any exists
else {
if (newParts.isEmpty()) {
continue;
}
DifferentPart previousDiffPart = (DifferentPart) newParts.getLast();
WordPart nextPart = i + 1 < parts.size() ? parts.get(i + 1) : null;
WordPart mergedPart = previousDiffPart.merge(part, nextPart);
if (mergedPart != null) {
i++;
newParts.remove(previousDiffPart);
newParts.add(mergedPart);
}
}
}
return newParts;
}
@Override
public String toString() {
return parts.stream().map(p -> p.toString()).collect(Collectors.joining());
}
/**
* Turns the LCS match into one or more words that do not match. This uses the common
* characters to build a mapping of the different words and their offsets into the new word
* originally passed to the WordDiffer.
*/
private TreeMap<Integer, String> buildWordOffsets(LcsMatch match) {
// break each word into parts, splitting on each character
TreeMap<Integer, String> wordsByOffset = new TreeMap<>();
StringBuilder buffy = new StringBuilder();
String word = match.newWord;
int wordIndex = 0; // index into the overall 'new word'
for (char c : match.lcs) {
for (; wordIndex < word.length(); wordIndex++) {
char wordChar = word.charAt(wordIndex);
if (wordChar == c) {
int offset = (wordIndex) - buffy.length();
saveWord(buffy, offset, wordsByOffset);
wordIndex++;
break;
}
buffy.append(wordChar);
}
}
int offset = wordIndex - buffy.length();
saveWord(buffy, offset, wordsByOffset);
if (wordIndex < word.length()) {
// the LCS ended; get the rest of the original word
buffy.append(word.substring(wordIndex));
saveWord(buffy, wordIndex, wordsByOffset);
}
return wordsByOffset;
}
private void saveWord(StringBuilder buffy, int charPosition,
TreeMap<Integer, String> wordIndices) {
if (buffy.length() > 0) {
wordIndices.put(charPosition, buffy.toString());
buffy.setLength(0);
}
}
private LcsMatch getLcs(String x, String y) {
StringReducingLcs lcs = new StringReducingLcs(x, y);
List<Character> lcsList = lcs.getLcs();
if (lcsList.isEmpty()) {
return null;
}
if (lcsList.size() < 3) {
return null; // what is the min size?
}
LcsMatch match = new LcsMatch(x, y, lcsList);
return match;
}
private List<WordPart> createWordParts(String newWord, TreeMap<Integer, String> wordIndices) {
List<WordPart> results = new ArrayList<>();
int lastWrittenIndex = 0;
Set<Entry<Integer, String>> entrySet = wordIndices.entrySet();
for (Entry<Integer, String> entry : entrySet) {
Integer index = entry.getKey();
if (lastWrittenIndex < index) {
String text = newWord.substring(lastWrittenIndex, index);
results.add(new SamePart(text, lastWrittenIndex));
}
String word = entry.getValue();
results.add(new DifferentPart(word, index));
lastWrittenIndex = index + word.length();
}
if (lastWrittenIndex < newWord.length()) {
String text = newWord.substring(lastWrittenIndex);
results.add(new SamePart(text, lastWrittenIndex));
}
return results;
}
//=================================================================================================
// Inner Classes
//=================================================================================================
/**
* A String that is part of a larger String. This class also has the offset into the original
* String.
*/
public abstract class WordPart {
protected String text;
protected int index;
WordPart(String text, int index) {
this.text = text;
this.index = index;
}
public int getIndex() {
return index;
}
public int length() {
return text.length();
}
public String getText() {
return text;
}
@Override
public String toString() {
return text;
}
}
public class SamePart extends WordPart {
SamePart(String text, int index) {
super(text, index);
}
}
public class DifferentPart extends WordPart {
DifferentPart(String text, int index) {
super(text, index);
}
public WordPart merge(WordPart oldPart, WordPart nextPart) {
if (!(nextPart instanceof DifferentPart)) {
return null;
}
String updatedText = text + oldPart.text + nextPart.text;
return new DifferentPart(updatedText, index);
}
@Override
public String toString() {
return " /" + text + "/ ";
}
}
private record LcsMatch(String newWord, String oldWord, List<Character> lcs) {}
}
@@ -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.
@@ -35,12 +35,12 @@ public class StringDiff {
/**
* String being inserted. This can be an insert or a complete replace (the positions will both
* be -1 in a replace; pos1 will be non-negative during an insert).
* be -1 in a replace; start will be non-negative during an insert).
*/
public String text;
/**
* Construct a new StringDiff with pos1 and pos2 are initialized to -1
* Construct a new StringDiff with start and end are initialized to -1
*
* @param newText string
* @return the new diff
@@ -50,7 +50,7 @@ public class StringDiff {
}
/**
* Construct a new StringDiff that indicates text was deleted from pos1 to pos2
* Construct a new StringDiff that indicates text was deleted from start and end
*
* @param start position 1 for the diff
* @param end position 2 for the diff
@@ -61,7 +61,7 @@ public class StringDiff {
}
/**
* Construct a new StringDiff that indicates that insertData was inserted at the given position
* Construct a new StringDiff that indicates that newText was inserted at the given position
*
* @param newText inserted string
* @param start position where the text was inserted
@@ -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.
@@ -219,9 +219,11 @@ class StringDiffUtils {
}
/**
* Applies the array of StringObjects to the string s to produce a new string. Warning - the
* diff objects cannot be applied to an arbitrary string, the Strings must be the original
* String used to compute the diffs.
* Applies the array of StringDiffs to the string s to produce a new string.
*
* <p>Warning: the diff objects cannot be applied to an arbitrary string, the Strings must be
* the original String used to compute the diffs.
*
* @param s the original string
* @param diffs the array of StringDiff object to apply
* @return a new String resulting from applying the diffs to s.
@@ -221,7 +221,8 @@ public class ApplicationVersion implements Comparable<ApplicationVersion> {
}
catch (NumberFormatException e) {
throw new IllegalArgumentException(
"Failed to convert " + versionPartName + " version to integer");
"Failed to convert version to integer: '" + versionPartName + "' value '" +
versionPart + "'");
}
}
}