IDEA-188650 diff: fix differences computation for empty trailing lines

This commit is contained in:
Aleksey Pivovarov
2018-03-22 20:02:18 +03:00
parent faf295c43c
commit 765bdf8cbe
9 changed files with 701 additions and 149 deletions
@@ -61,11 +61,34 @@ public class ComparisonManagerImpl extends ComparisonManager {
LineOffsets lineOffsets1 = LineOffsetsUtil.create(text1);
LineOffsets lineOffsets2 = LineOffsetsUtil.create(text2);
List<CharSequence> lineTexts1 = getLineContents(text1, lineOffsets1);
List<CharSequence> lineTexts2 = getLineContents(text2, lineOffsets2);
return compareLines(text1, text2, lineOffsets1, lineOffsets2, policy, indicator);
}
@NotNull
public List<LineFragment> compareLines(@NotNull CharSequence text1,
@NotNull CharSequence text2,
@NotNull LineOffsets lineOffsets1,
@NotNull LineOffsets lineOffsets2,
@NotNull ComparisonPolicy policy,
@NotNull ProgressIndicator indicator) throws DiffTooBigException {
Range range = new Range(0, lineOffsets1.getLineCount(),
0, lineOffsets2.getLineCount());
return compareLines(range, text1, text2, lineOffsets1, lineOffsets2, policy, indicator);
}
@NotNull
public List<LineFragment> compareLines(@NotNull Range range,
@NotNull CharSequence text1,
@NotNull CharSequence text2,
@NotNull LineOffsets lineOffsets1,
@NotNull LineOffsets lineOffsets2,
@NotNull ComparisonPolicy policy,
@NotNull ProgressIndicator indicator) throws DiffTooBigException {
List<CharSequence> lineTexts1 = getLineContents(range.start1, range.end1, text1, lineOffsets1);
List<CharSequence> lineTexts2 = getLineContents(range.start2, range.end2, text2, lineOffsets2);
FairDiffIterable iterable = ByLine.compare(lineTexts1, lineTexts2, policy, indicator);
return convertIntoLineFragments(lineOffsets1, lineOffsets2, iterable);
return convertIntoLineFragments(range, lineOffsets1, lineOffsets2, iterable);
}
@NotNull
@@ -97,6 +120,41 @@ public class ComparisonManagerImpl extends ComparisonManager {
return createInnerFragments(lineFragments, text1, text2, policy, indicator);
}
@NotNull
public List<LineFragment> compareLinesInner(@NotNull CharSequence text1,
@NotNull CharSequence text2,
@NotNull LineOffsets lineOffsets1,
@NotNull LineOffsets lineOffsets2,
@NotNull ComparisonPolicy policy,
boolean innerFragments,
@NotNull ProgressIndicator indicator) throws DiffTooBigException {
List<LineFragment> lineFragments = compareLines(text1, text2, lineOffsets1, lineOffsets2, policy, indicator);
if (innerFragments) {
return createInnerFragments(lineFragments, text1, text2, policy, indicator);
}
else {
return lineFragments;
}
}
@NotNull
public List<LineFragment> compareLinesInner(@NotNull Range range,
@NotNull CharSequence text1,
@NotNull CharSequence text2,
@NotNull LineOffsets lineOffsets1,
@NotNull LineOffsets lineOffsets2,
@NotNull ComparisonPolicy policy,
boolean innerFragments,
@NotNull ProgressIndicator indicator) throws DiffTooBigException {
List<LineFragment> lineFragments = compareLines(range, text1, text2, lineOffsets1, lineOffsets2, policy, indicator);
if (innerFragments) {
return createInnerFragments(lineFragments, text1, text2, policy, indicator);
}
else {
return lineFragments;
}
}
private static List<LineFragment> createInnerFragments(@NotNull List<LineFragment> lineFragments,
@NotNull CharSequence text1,
@NotNull CharSequence text2,
@@ -228,15 +286,21 @@ public class ComparisonManagerImpl extends ComparisonManager {
}
@NotNull
public static List<LineFragment> convertIntoLineFragments(@NotNull LineOffsets lineOffsets1,
public static List<LineFragment> convertIntoLineFragments(@NotNull Range range,
@NotNull LineOffsets lineOffsets1,
@NotNull LineOffsets lineOffsets2,
@NotNull FairDiffIterable changes) {
List<LineFragment> fragments = new ArrayList<>();
for (Range ch : changes.iterateChanges()) {
IntPair offsets1 = getOffsets(lineOffsets1, ch.start1, ch.end1);
IntPair offsets2 = getOffsets(lineOffsets2, ch.start2, ch.end2);
int startLine1 = ch.start1 + range.start1;
int startLine2 = ch.start2 + range.start2;
int endLine1 = ch.end1 + range.start1;
int endLine2 = ch.end2 + range.start2;
fragments.add(new LineFragmentImpl(ch.start1, ch.end1, ch.start2, ch.end2,
IntPair offsets1 = getOffsets(lineOffsets1, startLine1, endLine1);
IntPair offsets2 = getOffsets(lineOffsets2, startLine2, endLine2);
fragments.add(new LineFragmentImpl(startLine1, endLine1, startLine2, endLine2,
offsets1.val1, offsets1.val2, offsets2.val1, offsets2.val2));
}
return fragments;
@@ -419,21 +483,26 @@ public class ComparisonManagerImpl extends ComparisonManager {
@NotNull
private static List<CharSequence> getLineContents(@NotNull CharSequence text, @NotNull LineOffsets lineOffsets) {
List<CharSequence> lines = new ArrayList<>(lineOffsets.getLineCount());
for (int line = 0; line < lineOffsets.getLineCount(); line++) {
return getLineContents(0, lineOffsets.getLineCount(), text, lineOffsets);
}
@NotNull
private static List<CharSequence> getLineContents(int start, int end, @NotNull CharSequence text, @NotNull LineOffsets lineOffsets) {
List<CharSequence> lines = new ArrayList<>(end - start);
for (int line = start; line < end; line++) {
lines.add(new CharSequenceSubSequence(text, lineOffsets.getLineStart(line), lineOffsets.getLineEnd(line)));
}
return lines;
}
@NotNull
private static List<CharSequence> getNotIgnoredLineContents(@NotNull CharSequence text,
private static List<CharSequence> getNotIgnoredLineContents(int start, int end,
@NotNull CharSequence text,
@NotNull LineOffsets lineOffsets,
@NotNull BitSet ignored) {
StringBuilder sb = new StringBuilder();
List<CharSequence> lines = new ArrayList<>(lineOffsets.getLineCount());
for (int line = 0; line < lineOffsets.getLineCount(); line++) {
List<CharSequence> lines = new ArrayList<>(end - start);
for (int line = start; line < end; line++) {
for (int offset = lineOffsets.getLineStart(line); offset < lineOffsets.getLineEnd(line); offset++) {
if (ignored.get(offset)) continue;
sb.append(text.charAt(offset));
@@ -445,31 +514,43 @@ public class ComparisonManagerImpl extends ComparisonManager {
}
@NotNull
public List<LineFragment> compareLinesWithIgnoredRanges(@NotNull CharSequence text1,
@NotNull CharSequence text2,
@NotNull LineOffsets lineOffsets1,
@NotNull LineOffsets lineOffsets2,
@NotNull BitSet ignored1,
@NotNull BitSet ignored2,
boolean innerFragments,
@NotNull ProgressIndicator indicator) throws DiffTooBigException {
Range range = new Range(0, lineOffsets1.getLineCount(),
0, lineOffsets2.getLineCount());
return compareLinesWithIgnoredRanges(range, text1, text2, lineOffsets1, lineOffsets2, ignored1, ignored2,
innerFragments, indicator);
}
/**
* Compare two texts by-line and then compare changed fragments by-word
*/
@NotNull
public List<LineFragment> compareLinesWithIgnoredRanges(@NotNull CharSequence text1,
public List<LineFragment> compareLinesWithIgnoredRanges(@NotNull Range range,
@NotNull CharSequence text1,
@NotNull CharSequence text2,
@NotNull List<TextRange> ignoredRanges1,
@NotNull List<TextRange> ignoredRanges2,
@NotNull LineOffsets lineOffsets1,
@NotNull LineOffsets lineOffsets2,
@NotNull BitSet ignored1,
@NotNull BitSet ignored2,
boolean innerFragments,
@NotNull ProgressIndicator indicator) throws DiffTooBigException {
BitSet ignored1 = collectIgnoredRanges(ignoredRanges1);
BitSet ignored2 = collectIgnoredRanges(ignoredRanges2);
LineOffsets lineOffsets1 = LineOffsetsUtil.create(text1);
LineOffsets lineOffsets2 = LineOffsetsUtil.create(text2);
List<CharSequence> lineTexts1 = getNotIgnoredLineContents(text1, lineOffsets1, ignored1);
List<CharSequence> lineTexts2 = getNotIgnoredLineContents(text2, lineOffsets2, ignored2);
List<CharSequence> lineTexts1 = getNotIgnoredLineContents(range.start1, range.end1, text1, lineOffsets1, ignored1);
List<CharSequence> lineTexts2 = getNotIgnoredLineContents(range.start2, range.end2, text2, lineOffsets2, ignored2);
FairDiffIterable iterable = ByLine.compare(lineTexts1, lineTexts2, ComparisonPolicy.DEFAULT, indicator);
FairDiffIterable correctedIterable = correctIgnoredRangesSecondStep(iterable, text1, text2, lineOffsets1, lineOffsets2,
ignored1, ignored2);
List<LineFragment> lineFragments = convertIntoLineFragments(lineOffsets1, lineOffsets2, correctedIterable);
List<LineFragment> lineFragments = convertIntoLineFragments(range, lineOffsets1, lineOffsets2, correctedIterable);
if (innerFragments) {
lineFragments = createInnerFragments(lineFragments, text1, text2, ComparisonPolicy.DEFAULT, indicator);
@@ -497,7 +578,7 @@ public class ComparisonManagerImpl extends ComparisonManager {
@NotNull LineOffsets lineOffsets2,
@NotNull BitSet ignored1,
@NotNull BitSet ignored2) {
DiffIterableUtil.ChangeBuilder builder = new DiffIterableUtil.ChangeBuilder(lineOffsets1.getLineCount(), lineOffsets2.getLineCount());
DiffIterableUtil.ChangeBuilder builder = new DiffIterableUtil.ChangeBuilder(iterable.getLength1(), iterable.getLength2());
for (Range range : iterable.iterateUnchanged()) {
int count = range.end1 - range.start1;
for (int i = 0; i < count; i++) {
@@ -15,23 +15,20 @@
*/
package com.intellij.diff.tools.util.text;
import com.intellij.diff.comparison.ComparisonManager;
import com.intellij.diff.comparison.ComparisonManagerImpl;
import com.intellij.diff.comparison.ComparisonPolicy;
import com.intellij.diff.comparison.ComparisonUtil;
import com.intellij.diff.fragments.LineFragment;
import com.intellij.diff.fragments.LineFragmentImpl;
import com.intellij.diff.tools.util.base.HighlightPolicy;
import com.intellij.diff.tools.util.base.IgnorePolicy;
import com.intellij.diff.tools.util.base.TextDiffSettingsHolder.TextDiffSettings;
import com.intellij.diff.util.DiffUserDataKeysEx.DiffComputer;
import com.intellij.diff.util.DiffUtil;
import com.intellij.diff.util.Range;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.util.TextRange;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
@@ -39,40 +36,32 @@ import java.util.List;
import static com.intellij.diff.tools.util.base.HighlightPolicy.*;
import static com.intellij.diff.tools.util.base.IgnorePolicy.*;
import static java.util.Collections.singletonList;
public class SimpleTextDiffProvider extends TwosideTextDiffProviderBase implements TwosideTextDiffProvider {
static final DiffComputer DEFAULT_COMPUTER = (text1, text2, policy, innerChanges, indicator) -> {
if (innerChanges) {
return ComparisonManager.getInstance().compareLinesInner(text1, text2, policy, indicator);
}
else {
return ComparisonManager.getInstance().compareLines(text1, text2, policy, indicator);
}
};
private static final Logger LOG = Logger.getInstance(SimpleTextDiffProvider.class);
private static final IgnorePolicy[] IGNORE_POLICIES = {DEFAULT, TRIM_WHITESPACES, IGNORE_WHITESPACES, IGNORE_WHITESPACES_CHUNKS};
private static final HighlightPolicy[] HIGHLIGHT_POLICIES = {BY_LINE, BY_WORD, BY_WORD_SPLIT, DO_NOT_HIGHLIGHT};
@NotNull private final DiffComputer myDiffComputer;
@Nullable private final DiffComputer myDiffComputer;
public SimpleTextDiffProvider(@NotNull TextDiffSettings settings,
@NotNull Runnable rediff,
@NotNull Disposable disposable) {
this(settings, rediff, disposable, DEFAULT_COMPUTER);
this(settings, rediff, disposable, null);
}
public SimpleTextDiffProvider(@NotNull TextDiffSettings settings,
@NotNull Runnable rediff,
@NotNull Disposable disposable,
@NotNull DiffComputer diffComputer) {
@Nullable DiffComputer diffComputer) {
this(settings, rediff, disposable, diffComputer, IGNORE_POLICIES, HIGHLIGHT_POLICIES);
}
private SimpleTextDiffProvider(@NotNull TextDiffSettings settings,
@NotNull Runnable rediff,
@NotNull Disposable disposable,
@NotNull DiffComputer diffComputer,
@Nullable DiffComputer diffComputer,
@NotNull IgnorePolicy[] ignorePolicies,
@NotNull HighlightPolicy[] highlightPolicies) {
super(settings, rediff, disposable, ignorePolicies, highlightPolicies);
@@ -85,7 +74,7 @@ public class SimpleTextDiffProvider extends TwosideTextDiffProviderBase implemen
@NotNull CharSequence text2,
@NotNull LineOffsets lineOffsets1,
@NotNull LineOffsets lineOffsets2,
@NotNull List<Range> linesRanges,
@Nullable List<Range> linesRanges,
@NotNull IgnorePolicy ignorePolicy,
boolean innerFragments,
@NotNull ProgressIndicator indicator) {
@@ -93,73 +82,46 @@ public class SimpleTextDiffProvider extends TwosideTextDiffProviderBase implemen
}
@NotNull
public static List<List<LineFragment>> compareRange(@NotNull DiffComputer diffComputer,
public static List<List<LineFragment>> compareRange(@Nullable DiffComputer diffComputer,
@NotNull CharSequence text1,
@NotNull CharSequence text2,
@NotNull LineOffsets lineOffsets1,
@NotNull LineOffsets lineOffsets2,
@NotNull List<Range> linesRanges,
@Nullable List<Range> linesRanges,
@NotNull IgnorePolicy ignorePolicy,
boolean innerFragments,
@NotNull ProgressIndicator indicator) {
ComparisonPolicy policy = ignorePolicy.getComparisonPolicy();
List<List<LineFragment>> result = new ArrayList<>();
for (Range range : linesRanges) {
CharSequence content1 = DiffUtil.getLinesContent(text1, lineOffsets1, range.start1, range.end1, true);
CharSequence content2 = DiffUtil.getLinesContent(text2, lineOffsets2, range.start2, range.end2, true);
if (range.start1 == range.end1 || range.start2 == range.end2) {
boolean isEquals = ComparisonUtil.isEquals(content1, content2, policy);
result.add(singletonList(createSimpleFragment(range, lineOffsets1, lineOffsets2, isEquals)));
}
else {
List<LineFragment> fragments = diffComputer.compute(content1, content2, policy, innerFragments, indicator);
int startOffset1 = lineOffsets1.getLineStart(range.start1);
int startOffset2 = lineOffsets2.getLineStart(range.start2);
result.add(ContainerUtil.map(fragments, fragment -> {
return transferFragment(fragment, range.start1, range.start2, startOffset1, startOffset2);
}));
}
if (diffComputer != null && linesRanges != null) {
LOG.error(new Throwable("Unsupported operation: ranged diff with custom DiffComputer - " + diffComputer));
}
return result;
}
@NotNull
public static LineFragment createSimpleFragment(@NotNull Range linesRange,
@NotNull LineOffsets lineOffsets1,
@NotNull LineOffsets lineOffsets2,
boolean isEquals) {
TextRange textRange1 = DiffUtil.getLinesRange(lineOffsets1, linesRange.start1, linesRange.end1, true);
TextRange textRange2 = DiffUtil.getLinesRange(lineOffsets2, linesRange.start2, linesRange.end2, true);
return new LineFragmentImpl(linesRange.start1, linesRange.end1,
linesRange.start2, linesRange.end2,
textRange1.getStartOffset(), textRange1.getEndOffset(),
textRange2.getStartOffset(), textRange2.getEndOffset(),
isEquals ? Collections.emptyList() : null);
}
@NotNull
public static LineFragment transferFragment(@NotNull LineFragment fragment,
int startLine1, int startLine2,
int startOffset1, int startOffset2) {
if (startLine1 == 0 && startLine2 == 0 && startOffset1 == 0 && startOffset2 == 0) return fragment;
return new LineFragmentImpl(fragment.getStartLine1() + startLine1, fragment.getEndLine1() + startLine1,
fragment.getStartLine2() + startLine2, fragment.getEndLine2() + startLine2,
fragment.getStartOffset1() + startOffset1, fragment.getEndOffset1() + startOffset1,
fragment.getStartOffset2() + startOffset2, fragment.getEndOffset2() + startOffset2,
fragment.getInnerFragments());
ComparisonManagerImpl comparisonManager = ComparisonManagerImpl.getInstanceImpl();
if (diffComputer != null || linesRanges == null) {
List<LineFragment> fragments = comparisonManager.compareLinesInner(text1, text2, lineOffsets1, lineOffsets2,
policy, innerFragments, indicator);
return Collections.singletonList(fragments);
}
else {
List<List<LineFragment>> result = new ArrayList<>();
for (Range range : linesRanges) {
result.add(comparisonManager.compareLinesInner(range, text1, text2, lineOffsets1, lineOffsets2,
policy, innerFragments, indicator));
}
return result;
}
}
public static class NoIgnore extends SimpleTextDiffProvider implements TwosideTextDiffProvider.NoIgnore {
public NoIgnore(@NotNull TextDiffSettings settings, @NotNull Runnable rediff, @NotNull Disposable disposable) {
this(settings, rediff, disposable, DEFAULT_COMPUTER);
this(settings, rediff, disposable, null);
}
public NoIgnore(@NotNull TextDiffSettings settings, @NotNull Runnable rediff, @NotNull Disposable disposable, @NotNull DiffComputer diffComputer) {
public NoIgnore(@NotNull TextDiffSettings settings,
@NotNull Runnable rediff,
@NotNull Disposable disposable,
@Nullable DiffComputer diffComputer) {
super(settings, rediff, disposable, diffComputer, IGNORE_POLICIES, ArrayUtil.remove(HIGHLIGHT_POLICIES, DO_NOT_HIGHLIGHT));
}
@@ -23,7 +23,6 @@ import com.intellij.diff.requests.ContentDiffRequest;
import com.intellij.diff.tools.util.base.HighlightPolicy;
import com.intellij.diff.tools.util.base.IgnorePolicy;
import com.intellij.diff.tools.util.base.TextDiffSettingsHolder.TextDiffSettings;
import com.intellij.diff.util.DiffUtil;
import com.intellij.diff.util.Range;
import com.intellij.diff.util.Side;
import com.intellij.openapi.Disposable;
@@ -32,11 +31,12 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.List;
import static com.intellij.diff.tools.util.base.HighlightPolicy.*;
@@ -116,7 +116,7 @@ public class SmartTextDiffProvider extends TwosideTextDiffProviderBase implement
@NotNull CharSequence text2,
@NotNull LineOffsets lineOffsets1,
@NotNull LineOffsets lineOffsets2,
@NotNull List<Range> linesRanges,
@Nullable List<Range> linesRanges,
@NotNull IgnorePolicy ignorePolicy,
boolean innerFragments,
@NotNull ProgressIndicator indicator) {
@@ -124,8 +124,7 @@ public class SmartTextDiffProvider extends TwosideTextDiffProviderBase implement
return compareIgnoreFormatting(text1, text2, lineOffsets1, lineOffsets2, linesRanges, innerFragments, indicator);
}
else {
return SimpleTextDiffProvider.compareRange(SimpleTextDiffProvider.DEFAULT_COMPUTER,
text1, text2, lineOffsets1, lineOffsets2, linesRanges,
return SimpleTextDiffProvider.compareRange(null, text1, text2, lineOffsets1, lineOffsets2, linesRanges,
ignorePolicy, innerFragments, indicator);
}
}
@@ -135,47 +134,29 @@ public class SmartTextDiffProvider extends TwosideTextDiffProviderBase implement
@NotNull CharSequence text2,
@NotNull LineOffsets lineOffsets1,
@NotNull LineOffsets lineOffsets2,
@NotNull List<Range> linesRanges,
@Nullable List<Range> linesRanges,
boolean innerFragments,
@NotNull ProgressIndicator indicator) {
List<TextRange> ranges1 = myProvider.getIgnoredRanges(myProject, text1, myContent1);
List<TextRange> ranges2 = myProvider.getIgnoredRanges(myProject, text2, myContent2);
List<TextRange> ignoredRanges1 = myProvider.getIgnoredRanges(myProject, text1, myContent1);
List<TextRange> ignoredRanges2 = myProvider.getIgnoredRanges(myProject, text2, myContent2);
List<List<LineFragment>> result = new ArrayList<>();
for (Range range : linesRanges) {
TextRange offsets1 = DiffUtil.getLinesRange(lineOffsets1, range.start1, range.end1, true);
TextRange offsets2 = DiffUtil.getLinesRange(lineOffsets2, range.start2, range.end2, true);
BitSet ignored1 = ComparisonManagerImpl.collectIgnoredRanges(ignoredRanges1);
BitSet ignored2 = ComparisonManagerImpl.collectIgnoredRanges(ignoredRanges2);
CharSequence subText1 = offsets1.subSequence(text1);
CharSequence subText2 = offsets2.subSequence(text2);
List<TextRange> subRanges1 = getSubRanges(ranges1, offsets1);
List<TextRange> subRanges2 = getSubRanges(ranges2, offsets2);
ComparisonManagerImpl comparisonManager = ComparisonManagerImpl.getInstanceImpl();
List<LineFragment> fragments = comparisonManager.compareLinesWithIgnoredRanges(subText1, subText2, subRanges1, subRanges2,
innerFragments, indicator);
int startOffset1 = offsets1.getStartOffset();
int startOffset2 = offsets2.getStartOffset();
result.add(ContainerUtil.map(fragments, fragment -> {
return SimpleTextDiffProvider.transferFragment(fragment, range.start1, range.start2, startOffset1, startOffset2);
}));
ComparisonManagerImpl comparisonManager = ComparisonManagerImpl.getInstanceImpl();
if (linesRanges == null) {
List<LineFragment> fragments = comparisonManager.compareLinesWithIgnoredRanges(text1, text2, lineOffsets1, lineOffsets2,
ignored1, ignored2, innerFragments, indicator);
return Collections.singletonList(fragments);
}
return result;
}
@NotNull
private static List<TextRange> getSubRanges(@NotNull List<TextRange> ignoredRanges, @NotNull TextRange offsets) {
List<TextRange> result = new ArrayList<>();
for (TextRange range : ignoredRanges) {
TextRange intersection = range.intersection(offsets);
if (intersection != null) {
result.add(new TextRange(intersection.getStartOffset() - offsets.getStartOffset(),
intersection.getEndOffset() - offsets.getStartOffset()));
else {
List<List<LineFragment>> result = new ArrayList<>();
for (Range range : linesRanges) {
result.add(comparisonManager.compareLinesWithIgnoredRanges(range, text1, text2, lineOffsets1, lineOffsets2,
ignored1, ignored2, innerFragments, indicator));
}
return result;
}
return result;
}
@Nullable
@@ -30,8 +30,6 @@ import org.jetbrains.annotations.Nullable;
import java.util.List;
import static java.util.Collections.singletonList;
abstract class TwosideTextDiffProviderBase extends TextDiffProviderBase implements TwosideTextDiffProvider {
protected TwosideTextDiffProviderBase(@NotNull TextDiffSettings settings,
@NotNull Runnable rediff,
@@ -49,8 +47,7 @@ abstract class TwosideTextDiffProviderBase extends TextDiffProviderBase implemen
LineOffsets lineOffsets1 = LineOffsetsUtil.create(text1);
LineOffsets lineOffsets2 = LineOffsetsUtil.create(text2);
Range linesRange = new Range(0, lineOffsets1.getLineCount(), 0, lineOffsets2.getLineCount());
List<List<LineFragment>> fragments = doCompare(text1, text2, lineOffsets1, lineOffsets2, singletonList(linesRange), indicator);
List<List<LineFragment>> fragments = doCompare(text1, text2, lineOffsets1, lineOffsets2, null, indicator);
if (fragments == null) return null;
@@ -74,7 +71,7 @@ abstract class TwosideTextDiffProviderBase extends TextDiffProviderBase implemen
@NotNull CharSequence text2,
@NotNull LineOffsets lineOffsets1,
@NotNull LineOffsets lineOffsets2,
@NotNull List<Range> linesRanges,
@Nullable List<Range> linesRanges,
@NotNull ProgressIndicator indicator) {
IgnorePolicy ignorePolicy = getIgnorePolicy();
HighlightPolicy highlightPolicy = getHighlightPolicy();
@@ -89,7 +86,7 @@ abstract class TwosideTextDiffProviderBase extends TextDiffProviderBase implemen
indicator.checkCanceled();
List<List<LineFragment>> fragments = doCompare(text1, text2, lineOffsets1, lineOffsets2, linesRanges,
ignorePolicy, innerFragments, indicator);
assert fragments.size() == linesRanges.size();
assert fragments.size() == (linesRanges != null ? linesRanges.size() : 1);
indicator.checkCanceled();
return ContainerUtil.map(fragments, rangeFragments -> {
@@ -103,7 +100,7 @@ abstract class TwosideTextDiffProviderBase extends TextDiffProviderBase implemen
@NotNull CharSequence text2,
@NotNull LineOffsets lineOffsets1,
@NotNull LineOffsets lineOffsets2,
@NotNull List<Range> linesRanges,
@Nullable List<Range> linesRanges,
@NotNull IgnorePolicy ignorePolicy,
boolean innerFragments,
@NotNull ProgressIndicator indicator);
@@ -133,7 +133,7 @@ abstract class DiffTestCase : TestCase() {
fun textToReadableFormat(text: CharSequence?): String {
if (text == null) return "null"
return "'" + text.toString().replace('\n', '*').replace('\t', '+') + "'"
return "\"" + text.toString().replace('\n', '*').replace('\t', '+') + "\""
}
fun parseSource(string: CharSequence): String = string.toString().replace('_', '\n')
@@ -15,7 +15,14 @@
*/
package com.intellij.diff
import com.intellij.diff.fragments.LineFragment
import com.intellij.diff.tools.util.base.HighlightPolicy
import com.intellij.diff.tools.util.base.IgnorePolicy
import com.intellij.diff.tools.util.base.TextDiffSettingsHolder
import com.intellij.diff.tools.util.text.SimpleTextDiffProvider
import com.intellij.diff.util.Range
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.testFramework.fixtures.IdeaProjectTestFixture
import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory
import com.intellij.testFramework.runInEdtAndWait
@@ -47,4 +54,21 @@ abstract class HeavyDiffTestCase : DiffTestCase() {
super.runBare()
}
}
protected fun compareExplicitBlocks(text1: CharSequence, text2: CharSequence, ranges: List<Range>,
highlightPolicy: HighlightPolicy, ignorePolicy: IgnorePolicy): List<LineFragment> {
val settings = TextDiffSettingsHolder.TextDiffSettings()
settings.highlightPolicy = highlightPolicy
settings.ignorePolicy = ignorePolicy
val disposable = Disposer.newDisposable()
try {
val diffProvider = SimpleTextDiffProvider(settings, Runnable {}, disposable)
return diffProvider.compare(text1, text2, ranges, INDICATOR)!!.flatMap { it }
}
finally {
Disposer.dispose(disposable)
}
}
}
@@ -0,0 +1,364 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.diff.comparison
import com.intellij.diff.HeavyDiffTestCase
import com.intellij.diff.fragments.DiffFragmentImpl
import com.intellij.diff.fragments.LineFragment
import com.intellij.diff.tools.util.base.HighlightPolicy
import com.intellij.diff.tools.util.base.IgnorePolicy
import com.intellij.diff.util.Range
import com.intellij.diff.util.Side
import java.util.*
class BlocksComparisonUtilTest : HeavyDiffTestCase() {
fun `test simple blocks`() {
Test("X_a_Y_c_Z", "X_ a__Y__ c_Z")
.blocks(
Range(1, 2, 1, 3), Range(3, 4, 4, 6))
.expected(
Range(1, 2, 1, 3), Range(3, 4, 4, 6))
.default()
Test("X_a_Y_c_Z", "X_ a__Y__ c_Z")
.blocks(
Range(1, 2, 1, 3), Range(3, 4, 4, 6))
.expected(
Range(2, 2, 2, 3), Range(3, 3, 4, 5))
.trim()
.ignore()
Test("X_a_Y_c_Z", "X_ a__Y__ c_Z")
.blocks(
Range(1, 2, 1, 3), Range(3, 4, 4, 6))
.expected(
)
.ignore_chunks()
}
fun `test changes outside of blocks are ignored`() {
Test("X_a_Y_c_Z", "N_ a__N__ c_N")
.blocks(
Range(1, 2, 1, 3), Range(3, 4, 4, 6))
.expected(
Range(1, 2, 1, 3), Range(3, 4, 4, 6))
.default()
Test("X_X_X_a_Y_c_Z", "N_ a__N__ c_N")
.blocks(
Range(5, 6, 4, 6))
.expected(
Range(5, 6, 4, 6))
.default()
}
fun `test multiple fragments in a block`() {
Test("X_a_b_c_Y", "X_a1_ b_c2_Y")
.blocks(
Range(1, 4, 1, 4))
.expected(
Range(1, 4, 1, 4))
.default()
Test("X_a_b_c_Y", "X_a1_ b_c2_Y")
.blocks(
Range(1, 4, 1, 4))
.expected(
Range(1, 2, 1, 2), Range(3, 4, 3, 4))
.trim()
.ignore()
.ignore_chunks()
}
fun `test fragments respect ignore options`() {
Test("X_a_b_c_Y", "X_a1_ b_c2_Y")
.blocks(
Range(1, 4, 1, 4))
.expected(
Range(1, 4, 1, 4))
.default()
Test("X_Y", "X_a_c_Y")
.blocks(
Range(1, 1, 1, 3))
.expected(
Range(1, 1, 1, 3))
.default()
.trim()
.ignore()
.ignore_chunks()
Test("X_Y", "X___Y")
.blocks(
Range(1, 1, 1, 3))
.expected(
Range(1, 1, 1, 3))
.default()
.trim()
.ignore()
Test("X_Y", "X___Y")
.blocks(
Range(1, 1, 1, 3))
.expected(
)
.ignore_chunks()
}
fun `test leading-trailing empty lines`() {
Test("X_a__Y", "X__y_Y")
.blocks(
Range(1, 3, 1, 3))
.expected(
Range(1, 2, 1, 1), Range(3, 3, 2, 3))
.default()
.trim()
.ignore()
Test("X_a__", "X__y_")
.blocks(
Range(1, 3, 1, 3))
.expected(
Range(1, 2, 1, 1), Range(3, 3, 2, 3))
.default()
.trim()
.ignore()
Test("a__", "_y_")
.blocks(
Range(0, 2, 0, 2))
.expected(
Range(0, 1, 0, 0), Range(2, 2, 1, 2))
.default()
.trim()
.ignore()
Test("a__", "_y_")
.blocks(
Range(0, 3, 0, 3))
.expected(
Range(0, 1, 0, 0), Range(2, 2, 1, 2))
.default()
.trim()
.ignore()
Test("a_", "_y")
.blocks(
Range(0, 2, 0, 2))
.expected(
Range(0, 1, 0, 0), Range(2, 2, 1, 2))
.default()
.trim()
.ignore()
}
fun `test inner changes`() {
Test("X_a_Y", "X_b_Y",
" - ", " - ")
.blocks(
Range(1, 2, 1, 2)
)
.default()
.trim()
.ignore()
.ignore_chunks()
Test("X_a_Y", "X_ a_b_Y",
" ", " -- -- ")
.blocks(
Range(1, 2, 1, 3)
)
.default()
Test("X_a_Y", "X_ a_b_Y",
" ", " -- ")
.blocks(
Range(1, 2, 1, 3)
)
.expected(
Range(2, 2, 2, 3)
)
.trim()
.ignore()
Test("X_a b_Y", "X_a b_Y",
" ", " - ")
.blocks(
Range(1, 2, 1, 2)
)
.default()
.trim()
Test("X_a b_Y", "X_a b_Y",
" ", " ")
.blocks(
Range(1, 2, 1, 2)
)
.ignore()
.ignore_chunks()
Test("X_a_Y_c_Z", "X_ a__Y__ c_Z",
" ", " - - -- ")
.blocks(
Range(1, 2, 1, 3), Range(3, 4, 4, 6))
.expected(
Range(1, 2, 1, 3), Range(3, 4, 4, 6))
.default()
Test("X_a_Y_c_Z", "X_ a__Y__ c_Z",
" ", " - - ")
.blocks(
Range(1, 2, 1, 3), Range(3, 4, 4, 6))
.expected(
Range(2, 2, 2, 3), Range(3, 3, 4, 5))
.trim()
Test("X_a_Y_c_Z", "X_ a__Y__ c_Z",
" ", " ")
.blocks(
Range(1, 2, 1, 3), Range(3, 4, 4, 6))
.expected(
Range(2, 2, 2, 3), Range(3, 3, 4, 5))
.ignore()
}
fun `test inner changes for inserted empty line`() {
Test("X_Y", "X_ _Y",
" ", " -- ")
.blocks(
Range(1, 1, 1, 2)
)
.trim()
.default()
Test("X_Y", "X_ _Y",
" ", " ")
.blocks(
Range(1, 1, 1, 2)
)
.expected(
Range(1, 1, 1, 2)
)
.ignore()
Test("X_Y", "X_ _Y",
" ", " ")
.blocks(
Range(1, 1, 1, 2)
)
.expected(
)
.ignore_chunks()
}
fun `test inner changes with leading-trailing empty lines`() {
Test("X_a__Y", "X__y_Y",
" -- ", " -- ")
.blocks(
Range(1, 3, 1, 3))
.expected(
Range(1, 2, 1, 1), Range(3, 3, 2, 3))
.default()
.trim()
.ignore()
Test("a__", "_y_",
"-- ", " --")
.blocks(
Range(0, 2, 0, 2))
.expected(
Range(0, 1, 0, 0), Range(2, 2, 1, 2))
.default()
.trim()
.ignore()
}
private inner class Test(val input1: String, val input2: String,
val inner1: String? = null, val inner2: String? = null) {
var blocks: List<Range>? = null
var expected: List<Range>? = null
fun expected(vararg ranges: Range): Test {
expected = ranges.toList()
return this
}
fun blocks(vararg ranges: Range): Test {
blocks = ranges.toList()
return this
}
fun default(): Test {
doTest(IgnorePolicy.DEFAULT)
return this
}
fun trim(): Test {
doTest(IgnorePolicy.TRIM_WHITESPACES)
return this
}
fun ignore(): Test {
doTest(IgnorePolicy.IGNORE_WHITESPACES)
return this
}
fun ignore_chunks(): Test {
doTest(IgnorePolicy.IGNORE_WHITESPACES_CHUNKS)
return this
}
private fun doTest(ignorePolicy: IgnorePolicy) {
if (expected == null && (inner1 == null || inner2 == null)) throw IllegalArgumentException()
if (expected != null) {
val fragments1 = compareExplicitBlocks(parseSource(input1), parseSource(input2), blocks!!, HighlightPolicy.BY_LINE, ignorePolicy)
val fragments2 = compareExplicitBlocks(parseSource(input1), parseSource(input2), blocks!!, HighlightPolicy.BY_WORD, ignorePolicy)
assertEquals(expected!!, fragments1.toRanges())
assertEquals(expected!!, fragments2.toRanges())
}
if (inner1 != null && inner2 != null) {
val fragments = compareExplicitBlocks(parseSource(input1), parseSource(input2), blocks!!, HighlightPolicy.BY_WORD, ignorePolicy)
val expected1 = parseInnerExpected(inner1)
val expected2 = parseInnerExpected(inner2)
val actual1 = parseInnerActual(fragments, Side.LEFT)
val actual2 = parseInnerActual(fragments, Side.RIGHT)
assertEquals(expected1, actual1)
assertEquals(expected2, actual2)
}
}
private fun parseInnerExpected(inner: String): BitSet {
val set = BitSet()
inner.forEachIndexed { index, c ->
if (c == '-') set.set(index)
}
return set
}
private fun parseInnerActual(fragments: List<LineFragment>, side: Side): BitSet {
val set = BitSet()
fragments.flatMap { fragment ->
fragment.innerFragments?.map {
DiffFragmentImpl(fragment.startOffset1 + it.startOffset1, fragment.startOffset1 + it.endOffset1,
fragment.startOffset2 + it.startOffset2, fragment.startOffset2 + it.endOffset2)
} ?: listOf(fragment)
}.forEach {
val start = side.getStartOffset(it)
val end = side.getEndOffset(it)
set.set(start, end)
}
return set
}
private fun List<LineFragment>.toRanges(): List<Range> {
return this.map { Range(it.startLine1, it.endLine1, it.startLine2, it.endLine2) }
}
}
}
@@ -16,18 +16,23 @@
package com.intellij.diff.comparison
import com.intellij.diff.DiffTestCase
import com.intellij.diff.HeavyDiffTestCase
import com.intellij.diff.fragments.DiffFragment
import com.intellij.diff.fragments.LineFragment
import com.intellij.diff.fragments.MergeLineFragment
import com.intellij.diff.fragments.MergeWordFragment
import com.intellij.diff.tools.util.base.HighlightPolicy
import com.intellij.diff.tools.util.base.IgnorePolicy
import com.intellij.diff.util.DiffUtil
import com.intellij.diff.util.Range
import com.intellij.diff.util.ThreeSide
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.impl.DocumentImpl
import com.intellij.openapi.util.Couple
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vcs.ex.createRanges
class ComparisonUtilAutoTest : DiffTestCase() {
class ComparisonUtilAutoTest : HeavyDiffTestCase() {
val RUNS = 30
val MAX_LENGTH = 300
@@ -51,6 +56,10 @@ class ComparisonUtilAutoTest : DiffTestCase() {
doTestLineTrimSquashed(System.currentTimeMillis(), RUNS, MAX_LENGTH)
}
fun testExplicitBlocks() {
doTestExplicitBlocks(System.currentTimeMillis(), RUNS, MAX_LENGTH)
}
fun testMerge() {
doTestMerge(System.currentTimeMillis(), RUNS, MAX_LENGTH)
}
@@ -131,6 +140,31 @@ class ComparisonUtilAutoTest : DiffTestCase() {
}
}
private fun doTestExplicitBlocks(seed: Long, runs: Int, maxLength: Int) {
val ignorePolicies = listOf(IgnorePolicy.DEFAULT, IgnorePolicy.TRIM_WHITESPACES, IgnorePolicy.IGNORE_WHITESPACES, IgnorePolicy.IGNORE_WHITESPACES_CHUNKS)
val highlightPolicies = listOf(HighlightPolicy.BY_LINE, HighlightPolicy.BY_WORD, HighlightPolicy.BY_WORD_SPLIT)
doTest(seed, runs, maxLength) { text1, text2, debugData ->
for (highlightPolicy in highlightPolicies) {
for (ignorePolicy in ignorePolicies) {
debugData.put("HighlightPolicy", highlightPolicy)
debugData.put("IgnorePolicy", ignorePolicy)
val sequence1 = text1.charsSequence
val sequence2 = text2.charsSequence
val ranges = createRanges(sequence2, sequence1).map { Range(it.vcsLine1, it.vcsLine2, it.line1, it.line2) }
debugData.put("Ranges", ranges)
val fragments = compareExplicitBlocks(sequence1, sequence2, ranges, highlightPolicy, ignorePolicy)
debugData.put("Fragments", fragments)
checkResultLine(text1, text2, fragments, ignorePolicy.comparisonPolicy, !highlightPolicy.isShouldSquash)
}
}
}
}
private fun doTestMerge(seed: Long, runs: Int, maxLength: Int) {
val policies = listOf(ComparisonPolicy.DEFAULT, ComparisonPolicy.TRIM_WHITESPACES, ComparisonPolicy.IGNORE_WHITESPACES)
@@ -157,6 +191,16 @@ class ComparisonUtilAutoTest : DiffTestCase() {
private fun doTest(seed: Long, runs: Int, maxLength: Int, policies: List<ComparisonPolicy>,
test: (Document, Document, ComparisonPolicy, DiffTestCase.DebugData) -> Unit) {
doTest(seed, runs, maxLength) { text1, text2, debugData ->
for (comparisonPolicy in policies) {
debugData.put("Policy", comparisonPolicy)
test(text1, text2, comparisonPolicy, debugData)
}
}
}
private fun doTest(seed: Long, runs: Int, maxLength: Int,
test: (Document, Document, DiffTestCase.DebugData) -> Unit) {
doAutoTest(seed, runs) { debugData ->
debugData.put("MaxLength", maxLength)
@@ -166,10 +210,7 @@ class ComparisonUtilAutoTest : DiffTestCase() {
debugData.put("Text1", textToReadableFormat(text1.charsSequence))
debugData.put("Text2", textToReadableFormat(text2.charsSequence))
for (comparisonPolicy in policies) {
debugData.put("Policy", comparisonPolicy)
test(text1, text2, comparisonPolicy, debugData)
}
test(text1, text2, debugData)
}
}
@@ -17,7 +17,9 @@ package com.intellij.diff.comparison
import com.intellij.diff.DiffTestCase
import com.intellij.diff.fragments.LineFragment
import com.intellij.diff.tools.util.text.LineOffsetsUtil
import com.intellij.diff.util.IntPair
import com.intellij.diff.util.Range
import com.intellij.openapi.util.Couple
import com.intellij.openapi.util.TextRange
import java.util.*
@@ -411,6 +413,89 @@ class IgnoreComparisonUtilTest : DiffTestCase() {
.run()
}
fun `test explicit blocks`() {
Test("X_a_Y_b_Z", "X_a 1 c_Y_b 1 c_Z",
" ", " +++ +++ ",
" ", " - ")
.ranged(Range(1, 2, 1, 2))
.changedLinesNumber(1, 1)
.run()
Test("X_a_Y_b_Z", "X_a 1 c_Y_b 1 c_Z",
" ", " ++++ +++ ",
" ", " ")
.ranged(Range(1, 2, 1, 2))
.changedLinesNumber(0, 0)
.run()
Test("X_a_Y_b_Z", "X_a 1 c_Y_b 1 c_Z",
" ", " + ",
" ", " --- ")
.ranged(Range(3, 4, 3, 4))
.changedLinesNumber(1, 1)
.run()
Test("X_a_Y_b_Z", "X_a 1 c_Y_b 1 c_Z",
" ", " + ",
" - ", " ---- ")
.ranged(Range(1, 2, 3, 4))
.changedLinesNumber(1, 1)
.run()
Test("X_a_Y_b_Z", "Y_b 1 c_Z",
" ", " + ",
" ", " --- ")
.ranged(Range(3, 4, 1, 2))
.changedLinesNumber(1, 1)
.run()
Test("X_a_Y_b_Z", "Y_c_d_b_Z",
" ", " ++++ ",
" ", " ")
.ranged(Range(3, 4, 1, 4))
.changedLinesNumber(0, 0)
.run()
Test("X_a_Y_Z", "Y_c_d_Z",
" ", " ++++ ",
" ", " ")
.ranged(Range(3, 3, 1, 3))
.changedLinesNumber(0, 0)
.run()
Test("X_a_Y_Z", "Y_c_d_Z",
" ", " ",
" ", " ---- ")
.ranged(Range(3, 3, 1, 3))
.changedLinesNumber(0, 2)
.run()
Test("X_W 1 W_Y_W 2 W_Z", "X_W 3 W_B_W 4 W_Z",
" + + ", " + + ",
" -- ", " -- ")
.ranged(Range(2, 4, 2, 4))
.changedLinesNumber(1, 1)
.noInnerChanges()
.run()
Test("X_W 1 W_Y_W 2 W_Z", "X_W 3 W_B_W 4 W_Z",
" + + ", " + + ",
" -- ", " -- ")
.ranged(Range(1, 5, 1, 5))
.changedLinesNumber(1, 1)
.noInnerChanges()
.run()
Test("X_W 1 W_Y_W 2 W_Z", "X_W 3 W_B_W 4 W_Z",
" + + ", " + + ",
" ", " ")
.ranged(Range(1, 2, 3, 4))
.changedLinesNumber(0, 0)
.noInnerChanges()
.run()
}
private inner class Test(val input1: String, val input2: String,
ignored1: String, ignored2: String,
result1: String, result2: String) {
@@ -421,6 +506,7 @@ class IgnoreComparisonUtilTest : DiffTestCase() {
private var inner = true
private var changedLines: IntPair? = null
private var range: Range? = null
fun noInnerChanges(): Test {
inner = false
@@ -432,6 +518,11 @@ class IgnoreComparisonUtilTest : DiffTestCase() {
return this
}
fun ranged(range: Range): Test {
this.range = range
return this
}
fun run() {
assertEquals(input1.length, ignored1.length)
assertEquals(input1.length, result1.length)
@@ -444,7 +535,18 @@ class IgnoreComparisonUtilTest : DiffTestCase() {
val ignoredRanges1 = parseIgnored(ignored1)
val ignoredRanges2 = parseIgnored(ignored2)
val result = MANAGER.compareLinesWithIgnoredRanges(text1, text2, ignoredRanges1, ignoredRanges2, inner, INDICATOR)
val ignored1 = ComparisonManagerImpl.collectIgnoredRanges(ignoredRanges1)
val ignored2 = ComparisonManagerImpl.collectIgnoredRanges(ignoredRanges2)
val lineOffsets1 = LineOffsetsUtil.create(text1)
val lineOffsets2 = LineOffsetsUtil.create(text2)
val result = if (range != null) {
MANAGER.compareLinesWithIgnoredRanges(range!!, text1, text2, lineOffsets1, lineOffsets2, ignored1, ignored2, inner, INDICATOR)
}
else {
MANAGER.compareLinesWithIgnoredRanges(text1, text2, lineOffsets1, lineOffsets2, ignored1, ignored2, inner, INDICATOR)
}
val expected = Couple(parseExpected(result1), parseExpected(result2))
val actual = parseActual(result)