From f2508b2765a89e0f832cf28c10eaf41db52d3e0b Mon Sep 17 00:00:00 2001 From: dragonmacher <48328597+dragonmacher@users.noreply.github.com> Date: Fri, 20 Feb 2026 21:57:03 -0500 Subject: [PATCH] 1) Extracted word diffing code to be reusable 2) Fixed table column ordering bug for columns added after the model is created 3) Fixed potential exception when processing highlights before being realized 4) Search result highlight fixes to restore highlights when the search results are disposed 5) Fixed the Function Graph screenshots blocking issue --- .../AbstractFunctionGraphTest.java | 1 - .../docking/GlobalMenuAndToolBarManager.java | 7 +- .../widgets/fieldpanel/support/Highlight.java | 30 +- .../search/TextComponentSearchResults.java | 49 +++- .../widgets/search/TextComponentSearcher.java | 8 +- .../table/GDynamicColumnTableModel.java | 42 ++- .../widgets/table/TableColumnModelState.java | 21 +- .../table/VariableColumnTableModel.java | 8 +- .../generic/algorithms/StringReducingLcs.java | 41 +++ .../java/generic/algorithms/WordDiffer.java | 270 ++++++++++++++++++ .../program/database/code/StringDiff.java | 12 +- .../database/code/StringDiffUtils.java | 12 +- .../ghidra/framework/ApplicationVersion.java | 3 +- 13 files changed, 455 insertions(+), 49 deletions(-) create mode 100644 Ghidra/Framework/Generic/src/main/java/generic/algorithms/StringReducingLcs.java create mode 100644 Ghidra/Framework/Generic/src/main/java/generic/algorithms/WordDiffer.java 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 + "'"); } } }