diff --git a/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/AbstractFunctionGraphTest.java b/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/AbstractFunctionGraphTest.java
index ca991f3735..2303c67bde 100644
--- a/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/AbstractFunctionGraphTest.java
+++ b/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/AbstractFunctionGraphTest.java
@@ -401,7 +401,6 @@ public abstract class AbstractFunctionGraphTest extends AbstractGhidraHeadedInte
@After
public void tearDown() throws Exception {
waitForSwing();
- env.closeTool(tool);
env.dispose();
}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/GlobalMenuAndToolBarManager.java b/Ghidra/Framework/Docking/src/main/java/docking/GlobalMenuAndToolBarManager.java
index 1894e9df69..4b64f2f064 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/GlobalMenuAndToolBarManager.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/GlobalMenuAndToolBarManager.java
@@ -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;
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/Highlight.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/Highlight.java
index 39d72bb694..f45d17c824 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/Highlight.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/support/Highlight.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -17,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.
+ *
+ * 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);
+ }
}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/search/TextComponentSearchResults.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/search/TextComponentSearchResults.java
index d9d55af1bd..386556dc43 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/search/TextComponentSearchResults.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/search/TextComponentSearchResults.java
@@ -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);
+ }
}
}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/search/TextComponentSearcher.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/search/TextComponentSearcher.java
index 6965ea1249..5573de4538 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/search/TextComponentSearcher.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/search/TextComponentSearcher.java
@@ -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 lineRangeMap = mapLines(fullText);
Pattern pattern = createSearchPattern(searchText, useRegex);
@@ -252,7 +259,6 @@ public class TextComponentSearcher implements FindDialogSearcher {
context);
matchesByPosition.put(start, location);
}
-
}
private TreeMap mapLines(String fullText) {
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GDynamicColumnTableModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GDynamicColumnTableModel.java
index 22b90f8c2f..c852424007 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GDynamicColumnTableModel.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GDynamicColumnTableModel.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -62,10 +62,14 @@ public abstract class GDynamicColumnTableModel
protected ServiceProvider serviceProvider;
private TableColumnDescriptor columnDescriptor;
- protected List> tableColumns = new ArrayList<>();
- private List> defaultTableColumns = new ArrayList<>();
protected Map, Settings> columnSettings = new HashMap<>();
+ /** All currently visible columns */
+ protected List> tableColumns = new ArrayList<>();
+
+ /** The initially visible columns before user changes or state restoring */
+ private List> defaultTableColumns = new ArrayList<>();
+
private boolean ignoreSettingChanges = false;
public GDynamicColumnTableModel(ServiceProvider serviceProvider) {
@@ -298,7 +302,7 @@ public abstract class GDynamicColumnTableModel
protected void addTableColumns(Set> columns,
boolean isDefault) {
for (DynamicTableColumn column : columns) {
- doAddTableColumn(column, getDefaultTableColumns().size(), isDefault);
+ doAddTableColumn(column, -1, isDefault);
}
fireTableStructureChanged();
}
@@ -327,16 +331,30 @@ public abstract class GDynamicColumnTableModel
private void doAddTableColumn(DynamicTableColumn 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> 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> defaultColumns = getDefaultTableColumns();
+ defaultColumns.add(adjustedIndex, column);
}
/**
@@ -470,7 +488,7 @@ public abstract class GDynamicColumnTableModel
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 column =
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/TableColumnModelState.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/TableColumnModelState.java
index a367cd80e1..5bed5cde1e 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/TableColumnModelState.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/TableColumnModelState.java
@@ -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() {
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/VariableColumnTableModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/VariableColumnTableModel.java
index b469d25198..e2ef0f6d09 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/VariableColumnTableModel.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/VariableColumnTableModel.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -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();
/**
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/algorithms/StringReducingLcs.java b/Ghidra/Framework/Generic/src/main/java/generic/algorithms/StringReducingLcs.java
new file mode 100644
index 0000000000..f5b8a6bd63
--- /dev/null
+++ b/Ghidra/Framework/Generic/src/main/java/generic/algorithms/StringReducingLcs.java
@@ -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 {
+
+ 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);
+ }
+}
diff --git a/Ghidra/Framework/Generic/src/main/java/generic/algorithms/WordDiffer.java b/Ghidra/Framework/Generic/src/main/java/generic/algorithms/WordDiffer.java
new file mode 100644
index 0000000000..7712769b94
--- /dev/null
+++ b/Ghidra/Framework/Generic/src/main/java/generic/algorithms/WordDiffer.java
@@ -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 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 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 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 getMergedParts(int maxSize) {
+
+ List 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 buildWordOffsets(LcsMatch match) {
+ // break each word into parts, splitting on each character
+
+ TreeMap 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 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 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 createWordParts(String newWord, TreeMap wordIndices) {
+
+ List results = new ArrayList<>();
+
+ int lastWrittenIndex = 0;
+ Set> entrySet = wordIndices.entrySet();
+ for (Entry 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 lcs) {}
+}
diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/code/StringDiff.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/code/StringDiff.java
index ec9bb7062a..ab64fc3d9c 100644
--- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/code/StringDiff.java
+++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/code/StringDiff.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -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
diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/code/StringDiffUtils.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/code/StringDiffUtils.java
index 3edca3ac16..342594db98 100644
--- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/code/StringDiffUtils.java
+++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/code/StringDiffUtils.java
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -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.
+ *
+ * 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.
diff --git a/Ghidra/Framework/Utility/src/main/java/ghidra/framework/ApplicationVersion.java b/Ghidra/Framework/Utility/src/main/java/ghidra/framework/ApplicationVersion.java
index bf53b68224..39ae739e38 100644
--- a/Ghidra/Framework/Utility/src/main/java/ghidra/framework/ApplicationVersion.java
+++ b/Ghidra/Framework/Utility/src/main/java/ghidra/framework/ApplicationVersion.java
@@ -221,7 +221,8 @@ public class ApplicationVersion implements Comparable {
}
catch (NumberFormatException e) {
throw new IllegalArgumentException(
- "Failed to convert " + versionPartName + " version to integer");
+ "Failed to convert version to integer: '" + versionPartName + "' value '" +
+ versionPart + "'");
}
}
}