diff --git a/java-diff-utils/src/main/java/com/github/difflib/text/DiffRowGenerator.java b/java-diff-utils/src/main/java/com/github/difflib/text/DiffRowGenerator.java index 3711bfb6..e540004c 100644 --- a/java-diff-utils/src/main/java/com/github/difflib/text/DiffRowGenerator.java +++ b/java-diff-utils/src/main/java/com/github/difflib/text/DiffRowGenerator.java @@ -24,6 +24,8 @@ import com.github.difflib.patch.InsertDelta; import com.github.difflib.patch.Patch; import com.github.difflib.text.DiffRow.Tag; +import com.github.difflib.text.deltamerge.DeltaMergeUtils; +import com.github.difflib.text.deltamerge.InlineDeltaMergeInfo; import java.util.*; import java.util.function.BiFunction; import java.util.function.BiPredicate; @@ -75,6 +77,14 @@ public final class DiffRowGenerator { public static final Function> SPLITTER_BY_WORD = line -> splitStringPreserveDelimiter(line, SPLIT_BY_WORD_PATTERN); public static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+"); + public static final Function>> DEFAULT_INLINE_DELTA_MERGER = InlineDeltaMergeInfo::getDeltas; + + /** + * Merge diffs which are separated by equalities consisting of whitespace only. + */ + public static final Function>> WHITESPACE_EQUALITIES_MERGER = deltaMergeInfo -> DeltaMergeUtils + .mergeInlineDeltas(deltaMergeInfo, (equalities -> equalities.stream().allMatch(String::isBlank))); + public static Builder create() { return new Builder(); } @@ -170,6 +180,7 @@ static void wrapInTag(List sequence, int startPosition, private final boolean reportLinesUnchanged; private final Function lineNormalizer; private final Function processDiffs; + private final Function>> inlineDeltaMerger; private final boolean showInlineDiffs; private final boolean replaceOriginalLinefeedInChangesWithSpaces; @@ -194,11 +205,13 @@ private DiffRowGenerator(Builder builder) { reportLinesUnchanged = builder.reportLinesUnchanged; lineNormalizer = builder.lineNormalizer; processDiffs = builder.processDiffs; + inlineDeltaMerger = builder.inlineDeltaMerger; replaceOriginalLinefeedInChangesWithSpaces = builder.replaceOriginalLinefeedInChangesWithSpaces; Objects.requireNonNull(inlineDiffSplitter); Objects.requireNonNull(lineNormalizer); + Objects.requireNonNull(inlineDeltaMerger); } /** @@ -370,7 +383,10 @@ private List generateInlineDiffs(AbstractDelta delta) { origList = inlineDiffSplitter.apply(joinedOrig); revList = inlineDiffSplitter.apply(joinedRev); - List> inlineDeltas = DiffUtils.diff(origList, revList, equalizer).getDeltas(); + List> originalInlineDeltas = DiffUtils.diff(origList, revList, equalizer) + .getDeltas(); + List> inlineDeltas = inlineDeltaMerger + .apply(new InlineDeltaMergeInfo(originalInlineDeltas, origList, revList)); Collections.reverse(inlineDeltas); for (AbstractDelta inlineDelta : inlineDeltas) { @@ -465,6 +481,7 @@ public static class Builder { private Function processDiffs = null; private BiPredicate equalizer = null; private boolean replaceOriginalLinefeedInChangesWithSpaces = false; + private Function>> inlineDeltaMerger = DEFAULT_INLINE_DELTA_MERGER; private Builder() { } @@ -673,5 +690,17 @@ public Builder replaceOriginalLinefeedInChangesWithSpaces(boolean replace) { this.replaceOriginalLinefeedInChangesWithSpaces = replace; return this; } + + /** + * Provide an inline delta merger for use case specific delta optimizations. + * + * @param inlineDeltaMerger + * @return + */ + public Builder inlineDeltaMerger( + Function>> inlineDeltaMerger) { + this.inlineDeltaMerger = inlineDeltaMerger; + return this; + } } } diff --git a/java-diff-utils/src/main/java/com/github/difflib/text/deltamerge/DeltaMergeUtils.java b/java-diff-utils/src/main/java/com/github/difflib/text/deltamerge/DeltaMergeUtils.java new file mode 100644 index 00000000..0d9a0a16 --- /dev/null +++ b/java-diff-utils/src/main/java/com/github/difflib/text/deltamerge/DeltaMergeUtils.java @@ -0,0 +1,79 @@ +/* + * Copyright 2009-2024 java-diff-utils. + * + * 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 com.github.difflib.text.deltamerge; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +import com.github.difflib.patch.AbstractDelta; +import com.github.difflib.patch.ChangeDelta; +import com.github.difflib.patch.Chunk; + +/** + * Provides utility features for merge inline deltas + * + * @author Christian Meier + */ +final public class DeltaMergeUtils { + + public static List> mergeInlineDeltas(InlineDeltaMergeInfo deltaMergeInfo, + Predicate> replaceEquality) { + final List> originalDeltas = deltaMergeInfo.getDeltas(); + if (originalDeltas.size() < 2) { + return originalDeltas; + } + + final List> newDeltas = new ArrayList<>(); + newDeltas.add(originalDeltas.get(0)); + for (int i = 1; i < originalDeltas.size(); i++) { + final AbstractDelta previousDelta = newDeltas.getLast(); + final AbstractDelta currentDelta = originalDeltas.get(i); + + final List equalities = deltaMergeInfo.getOrigList().subList( + previousDelta.getSource().getPosition() + previousDelta.getSource().size(), + currentDelta.getSource().getPosition()); + + if (replaceEquality.test(equalities)) { + // Merge the previous delta, the equality and the current delta into one + // ChangeDelta and replace the previous delta by this new ChangeDelta. + final List allSourceLines = new ArrayList<>(); + allSourceLines.addAll(previousDelta.getSource().getLines()); + allSourceLines.addAll(equalities); + allSourceLines.addAll(currentDelta.getSource().getLines()); + + final List allTargetLines = new ArrayList<>(); + allTargetLines.addAll(previousDelta.getTarget().getLines()); + allTargetLines.addAll(equalities); + allTargetLines.addAll(currentDelta.getTarget().getLines()); + + final ChangeDelta replacement = new ChangeDelta<>( + new Chunk<>(previousDelta.getSource().getPosition(), allSourceLines), + new Chunk<>(previousDelta.getTarget().getPosition(), allTargetLines)); + + newDeltas.removeLast(); + newDeltas.add(replacement); + } else { + newDeltas.add(currentDelta); + } + } + + return newDeltas; + } + + private DeltaMergeUtils() { + } +} diff --git a/java-diff-utils/src/main/java/com/github/difflib/text/deltamerge/InlineDeltaMergeInfo.java b/java-diff-utils/src/main/java/com/github/difflib/text/deltamerge/InlineDeltaMergeInfo.java new file mode 100644 index 00000000..cc6b399a --- /dev/null +++ b/java-diff-utils/src/main/java/com/github/difflib/text/deltamerge/InlineDeltaMergeInfo.java @@ -0,0 +1,51 @@ +/* + * Copyright 2009-2024 java-diff-utils. + * + * 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 com.github.difflib.text.deltamerge; + +import java.util.List; + +import com.github.difflib.patch.AbstractDelta; + +/** + * Holds the information required to merge deltas originating from an inline + * diff + * + * @author Christian Meier + */ +public final class InlineDeltaMergeInfo { + + private final List> deltas; + private final List origList; + private final List revList; + + public InlineDeltaMergeInfo(List> deltas, List origList, List revList) { + this.deltas = deltas; + this.origList = origList; + this.revList = revList; + } + + public List> getDeltas() { + return deltas; + } + + public List getOrigList() { + return origList; + } + + public List getRevList() { + return revList; + } +} diff --git a/java-diff-utils/src/test/java/com/github/difflib/text/DiffRowGeneratorTest.java b/java-diff-utils/src/test/java/com/github/difflib/text/DiffRowGeneratorTest.java index f881c474..76c03044 100644 --- a/java-diff-utils/src/test/java/com/github/difflib/text/DiffRowGeneratorTest.java +++ b/java-diff-utils/src/test/java/com/github/difflib/text/DiffRowGeneratorTest.java @@ -12,6 +12,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.function.Function; import java.util.regex.Pattern; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; @@ -20,6 +21,10 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; +import com.github.difflib.patch.AbstractDelta; +import com.github.difflib.text.deltamerge.DeltaMergeUtils; +import com.github.difflib.text.deltamerge.InlineDeltaMergeInfo; + public class DiffRowGeneratorTest { @Test @@ -791,6 +796,47 @@ public void testIssue129SkipWhitespaceChanges() throws IOException { .forEach(System.out::println); } + @Test + public void testGeneratorWithWhitespaceDeltaMerge() { + final DiffRowGenerator generator = DiffRowGenerator.create().showInlineDiffs(true).mergeOriginalRevised(true) + .inlineDiffByWord(true).oldTag(f -> "~").newTag(f -> "**") // + .lineNormalizer(StringUtils::htmlEntites) // do not replace tabs + .inlineDeltaMerger(DiffRowGenerator.WHITESPACE_EQUALITIES_MERGER).build(); + + assertInlineDiffResult(generator, "No diff", "No diff", "No diff"); + assertInlineDiffResult(generator, " x whitespace before diff", " y whitespace before diff", + " ~x~**y** whitespace before diff"); + assertInlineDiffResult(generator, "Whitespace after diff x ", "Whitespace after diff y ", + "Whitespace after diff ~x~**y** "); + assertInlineDiffResult(generator, "Diff x x between", "Diff y y between", "Diff ~x x~**y y** between"); + assertInlineDiffResult(generator, "Hello \t world", "Hi \t universe", "~Hello \t world~**Hi \t universe**"); + assertInlineDiffResult(generator, "The quick brown fox jumps over the lazy dog", "A lazy dog jumps over a fox", + "~The quick brown fox ~**A lazy dog **jumps over ~the lazy dog~**a fox**"); + } + + @Test + public void testGeneratorWithMergingDeltasForShortEqualities() { + final Function>> shortEqualitiesMerger = deltaMergeInfo -> DeltaMergeUtils + .mergeInlineDeltas(deltaMergeInfo, + (equalities -> equalities.stream().mapToInt(String::length).sum() < 6)); + + final DiffRowGenerator generator = DiffRowGenerator.create().showInlineDiffs(true).mergeOriginalRevised(true) + .inlineDiffByWord(true).oldTag(f -> "~").newTag(f -> "**").inlineDeltaMerger(shortEqualitiesMerger) + .build(); + + assertInlineDiffResult(generator, "No diff", "No diff", "No diff"); + assertInlineDiffResult(generator, "aaa bbb ccc", "xxx bbb zzz", "~aaa bbb ccc~**xxx bbb zzz**"); + assertInlineDiffResult(generator, "aaa bbbb ccc", "xxx bbbb zzz", "~aaa~**xxx** bbbb ~ccc~**zzz**"); + } + + private void assertInlineDiffResult(DiffRowGenerator generator, String original, String revised, String expected) { + final List rows = generator.generateDiffRows(Arrays.asList(original), Arrays.asList(revised)); + print(rows); + + assertEquals(1, rows.size()); + assertEquals(expected, rows.get(0).getOldLine().toString()); + } + @Test public void testIssue188HangOnExamples() throws IOException, URISyntaxException { try (FileSystem zipFs = FileSystems.newFileSystem(Paths.get("target/test-classes/com/github/difflib/text/test.zip"), null);) {