/*
* Copyright 2000-2015 JetBrains s.r.o.
*
* 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.intellij.diff.comparison;
import com.intellij.diff.comparison.iterables.DiffIterable;
import com.intellij.diff.comparison.iterables.FairDiffIterable;
import com.intellij.diff.fragments.*;
import com.intellij.diff.util.IntPair;
import com.intellij.diff.util.MergeRange;
import com.intellij.diff.util.Range;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.Consumer;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.diff.FilesTooBigForDiffException;
import com.intellij.util.text.CharSequenceSubSequence;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ComparisonManagerImpl extends ComparisonManager {
public static final Logger LOG = Logger.getInstance(ComparisonManagerImpl.class);
@NotNull
@Override
public List<LineFragment> compareLines(@NotNull CharSequence text1,
@NotNull CharSequence text2,
@NotNull ComparisonPolicy policy,
@NotNull ProgressIndicator indicator) throws DiffTooBigException {
List<Line> lines1 = getLines(text1);
List<Line> lines2 = getLines(text2);
FairDiffIterable iterable = ByLine.compare(lines1, lines2, policy, indicator);
return convertIntoLineFragments(lines1, lines2, iterable);
}
@NotNull
@Override
public List<MergeLineFragment> compareLines(@NotNull CharSequence text1,
@NotNull CharSequence text2,
@NotNull CharSequence text3,
@NotNull ComparisonPolicy policy,
@NotNull ProgressIndicator indicator) throws DiffTooBigException {
List<Line> lines1 = getLines(text1);
List<Line> lines2 = getLines(text2);
List<Line> lines3 = getLines(text3);
List<MergeRange> ranges = ByLine.compare(lines1, lines2, lines3, policy, indicator);
return convertIntoMergeLineFragments(ranges);
}
@NotNull
@Override
public List<LineFragment> compareLinesInner(@NotNull CharSequence text1,
@NotNull CharSequence text2,
@NotNull ComparisonPolicy policy,
@NotNull ProgressIndicator indicator) throws DiffTooBigException {
List<LineFragment> lineFragments = compareLines(text1, text2, policy, indicator);
List<LineFragment> fineFragments = new ArrayList<>(lineFragments.size());
int tooBigChunksCount = 0;
for (LineFragment fragment : lineFragments) {
CharSequence subSequence1 = text1.subSequence(fragment.getStartOffset1(), fragment.getEndOffset1());
CharSequence subSequence2 = text2.subSequence(fragment.getStartOffset2(), fragment.getEndOffset2());
if (fragment.getStartLine1() == fragment.getEndLine1() ||
fragment.getStartLine2() == fragment.getEndLine2()) { // Insertion / Deletion
if (isEquals(subSequence1, subSequence2, policy)) {
fineFragments.add(new LineFragmentImpl(fragment, Collections.<DiffFragment>emptyList()));
}
else {
fineFragments.add(new LineFragmentImpl(fragment, null));
}
continue;
}
if (tooBigChunksCount >= FilesTooBigForDiffException.MAX_BAD_LINES) { // Do not try to build fine blocks after few fails)
fineFragments.add(new LineFragmentImpl(fragment, null));
continue;
}
try {
List<ByWord.LineBlock> lineBlocks = ByWord.compareAndSplit(subSequence1, subSequence2, policy, indicator);
assert lineBlocks.size() != 0;
int startOffset1 = fragment.getStartOffset1();
int startOffset2 = fragment.getStartOffset2();
int currentStartLine1 = fragment.getStartLine1();
int currentStartLine2 = fragment.getStartLine2();
for (int i = 0; i < lineBlocks.size(); i++) {
ByWord.LineBlock block = lineBlocks.get(i);
Range offsets = block.offsets;
// special case for last line to void problem with empty last line
int currentEndLine1 = i != lineBlocks.size() - 1 ? currentStartLine1 + block.newlines1 : fragment.getEndLine1();
int currentEndLine2 = i != lineBlocks.size() - 1 ? currentStartLine2 + block.newlines2 : fragment.getEndLine2();
fineFragments.add(new LineFragmentImpl(currentStartLine1, currentEndLine1, currentStartLine2, currentEndLine2,
offsets.start1 + startOffset1, offsets.end1 + startOffset1,
offsets.start2 + startOffset2, offsets.end2 + startOffset2,
block.fragments));
currentStartLine1 = currentEndLine1;
currentStartLine2 = currentEndLine2;
}
}
catch (DiffTooBigException e) {
fineFragments.add(new LineFragmentImpl(fragment, null));
tooBigChunksCount++;
}
}
return fineFragments;
}
@NotNull
@Override
@Deprecated
public List<LineFragment> compareLinesInner(@NotNull CharSequence text1,
@NotNull CharSequence text2,
@NotNull List<LineFragment> lineFragments,
@NotNull ComparisonPolicy policy,
@NotNull ProgressIndicator indicator) throws DiffTooBigException {
return compareLinesInner(text1, text2, policy, indicator);
}
@NotNull
@Override
public List<DiffFragment> compareWords(@NotNull CharSequence text1,
@NotNull CharSequence text2,
@NotNull ComparisonPolicy policy,
@NotNull ProgressIndicator indicator) throws DiffTooBigException {
return ByWord.compare(text1, text2, policy, indicator);
}
@NotNull
@Override
public List<DiffFragment> compareChars(@NotNull CharSequence text1,
@NotNull CharSequence text2,
@NotNull ComparisonPolicy policy,
@NotNull ProgressIndicator indicator) throws DiffTooBigException {
if (policy == ComparisonPolicy.IGNORE_WHITESPACES) {
return convertIntoDiffFragments(ByChar.compareIgnoreWhitespaces(text1, text2, indicator));
}
if (policy == ComparisonPolicy.DEFAULT) {
return convertIntoDiffFragments(ByChar.compareTwoStep(text1, text2, indicator));
}
LOG.warn(policy.toString() + " is not supported by ByChar comparison");
return convertIntoDiffFragments(ByChar.compareTwoStep(text1, text2, indicator));
}
@NotNull
public List<Range> compareLines(@NotNull List<? extends CharSequence> lines1,
@NotNull List<? extends CharSequence> lines2,
@NotNull ComparisonPolicy policy,
@NotNull ProgressIndicator indicator) throws DiffTooBigException {
FairDiffIterable iterable = ByLine.compare(lines1, lines2, policy, indicator);
return ContainerUtil.newArrayList(iterable.iterateChanges());
}
@Override
public boolean isEquals(@NotNull CharSequence text1, @NotNull CharSequence text2, @NotNull ComparisonPolicy policy) {
return ComparisonUtil.isEquals(text1, text2, policy);
}
//
// Fragments
//
@NotNull
public static List<DiffFragment> convertIntoDiffFragments(@NotNull DiffIterable changes) {
final List<DiffFragment> fragments = new ArrayList<>();
for (Range ch : changes.iterateChanges()) {
fragments.add(new DiffFragmentImpl(ch.start1, ch.end1, ch.start2, ch.end2));
}
return fragments;
}
@NotNull
public static List<LineFragment> convertIntoLineFragments(@NotNull List<Line> lines1,
@NotNull List<Line> lines2,
@NotNull FairDiffIterable changes) {
List<LineFragment> fragments = new ArrayList<>();
for (Range ch : changes.iterateChanges()) {
IntPair offsets1 = getOffsets(lines1, ch.start1, ch.end1);
IntPair offsets2 = getOffsets(lines2, ch.start2, ch.end2);
fragments.add(new LineFragmentImpl(ch.start1, ch.end1, ch.start2, ch.end2,
offsets1.val1, offsets1.val2, offsets2.val1, offsets2.val2));
}
return fragments;
}
@NotNull
private static IntPair getOffsets(@NotNull List<Line> lines, int startIndex, int endIndex) {
if (startIndex == endIndex) {
int offset;
if (startIndex < lines.size()) {
offset = lines.get(startIndex).getOffset1();
}
else {
offset = lines.get(lines.size() - 1).getOffset2();
}
return new IntPair(offset, offset);
}
else {
int offset1 = lines.get(startIndex).getOffset1();
int offset2 = lines.get(endIndex - 1).getOffset2();
return new IntPair(offset1, offset2);
}
}
@NotNull
public static List<MergeLineFragment> convertIntoMergeLineFragments(@NotNull List<MergeRange> conflicts) {
return ContainerUtil.map(conflicts, ch -> new MergeLineFragmentImpl(ch.start1, ch.end1, ch.start2, ch.end2, ch.start3, ch.end3));
}
@NotNull
public static List<MergeWordFragment> convertIntoMergeWordFragments(@NotNull List<MergeRange> conflicts) {
return ContainerUtil.map(conflicts, ch -> new MergeWordFragmentImpl(ch.start1, ch.end1, ch.start2, ch.end2, ch.start3, ch.end3));
}
//
// Post process line fragments
//
@NotNull
@Override
public List<LineFragment> squash(@NotNull List<LineFragment> oldFragments) {
if (oldFragments.isEmpty()) return oldFragments;
final List<LineFragment> newFragments = new ArrayList<>();
processAdjoining(oldFragments, fragments -> newFragments.add(doSquash(fragments)));
return newFragments;
}
@NotNull
@Override
public List<LineFragment> processBlocks(@NotNull List<LineFragment> oldFragments,
@NotNull final CharSequence text1, @NotNull final CharSequence text2,
@NotNull final ComparisonPolicy policy,
final boolean squash, final boolean trim) {
if (!squash && !trim) return oldFragments;
if (oldFragments.isEmpty()) return oldFragments;
final List<LineFragment> newFragments = new ArrayList<>();
processAdjoining(oldFragments, fragments -> newFragments.addAll(processAdjoining(fragments, text1, text2, policy, squash, trim)));
return newFragments;
}
private static void processAdjoining(@NotNull List<LineFragment> oldFragments,
@NotNull Consumer<List<LineFragment>> consumer) {
int startIndex = 0;
for (int i = 1; i < oldFragments.size(); i++) {
if (!isAdjoining(oldFragments.get(i - 1), oldFragments.get(i))) {
consumer.consume(oldFragments.subList(startIndex, i));
startIndex = i;
}
}
if (startIndex < oldFragments.size()) {
consumer.consume(oldFragments.subList(startIndex, oldFragments.size()));
}
}
@NotNull
private static List<LineFragment> processAdjoining(@NotNull List<LineFragment> fragments,
@NotNull CharSequence text1, @NotNull CharSequence text2,
@NotNull ComparisonPolicy policy, boolean squash, boolean trim) {
int start = 0;
int end = fragments.size();
// TODO: trim empty leading/trailing lines
if (trim && policy == ComparisonPolicy.IGNORE_WHITESPACES) {
while (start < end) {
LineFragment fragment = fragments.get(start);
CharSequenceSubSequence sequence1 = new CharSequenceSubSequence(text1, fragment.getStartOffset1(), fragment.getEndOffset1());
CharSequenceSubSequence sequence2 = new CharSequenceSubSequence(text2, fragment.getStartOffset2(), fragment.getEndOffset2());
if ((fragment.getInnerFragments() == null || !fragment.getInnerFragments().isEmpty()) &&
!StringUtil.equalsIgnoreWhitespaces(sequence1, sequence2)) {
break;
}
start++;
}
while (start < end) {
LineFragment fragment = fragments.get(end - 1);
CharSequenceSubSequence sequence1 = new CharSequenceSubSequence(text1, fragment.getStartOffset1(), fragment.getEndOffset1());
CharSequenceSubSequence sequence2 = new CharSequenceSubSequence(text2, fragment.getStartOffset2(), fragment.getEndOffset2());
if ((fragment.getInnerFragments() == null || !fragment.getInnerFragments().isEmpty()) &&
!StringUtil.equalsIgnoreWhitespaces(sequence1, sequence2)) {
break;
}
end--;
}
}
if (start == end) return Collections.emptyList();
if (squash) {
return Collections.singletonList(doSquash(fragments.subList(start, end)));
}
return fragments.subList(start, end);
}
@NotNull
private static LineFragment doSquash(@NotNull List<LineFragment> oldFragments) {
assert !oldFragments.isEmpty();
if (oldFragments.size() == 1) return oldFragments.get(0);
LineFragment firstFragment = oldFragments.get(0);
LineFragment lastFragment = oldFragments.get(oldFragments.size() - 1);
List<DiffFragment> newInnerFragments = new ArrayList<>();
for (LineFragment fragment : oldFragments) {
for (DiffFragment innerFragment : extractInnerFragments(fragment)) {
int shift1 = fragment.getStartOffset1() - firstFragment.getStartOffset1();
int shift2 = fragment.getStartOffset2() - firstFragment.getStartOffset2();
DiffFragment previousFragment = ContainerUtil.getLastItem(newInnerFragments);
if (previousFragment == null || !isAdjoiningInner(previousFragment, innerFragment, shift1, shift2)) {
newInnerFragments.add(new DiffFragmentImpl(innerFragment.getStartOffset1() + shift1, innerFragment.getEndOffset1() + shift1,
innerFragment.getStartOffset2() + shift2, innerFragment.getEndOffset2() + shift2));
}
else {
newInnerFragments.remove(newInnerFragments.size() - 1);
newInnerFragments.add(new DiffFragmentImpl(previousFragment.getStartOffset1(), innerFragment.getEndOffset1() + shift1,
previousFragment.getStartOffset2(), innerFragment.getEndOffset2() + shift2));
}
}
}
return new LineFragmentImpl(firstFragment.getStartLine1(), lastFragment.getEndLine1(),
firstFragment.getStartLine2(), lastFragment.getEndLine2(),
firstFragment.getStartOffset1(), lastFragment.getEndOffset1(),
firstFragment.getStartOffset2(), lastFragment.getEndOffset2(),
newInnerFragments);
}
private static boolean isAdjoining(@NotNull LineFragment beforeFragment, @NotNull LineFragment afterFragment) {
if (beforeFragment.getEndLine1() != afterFragment.getStartLine1() ||
beforeFragment.getEndLine2() != afterFragment.getStartLine2() ||
beforeFragment.getEndOffset1() != afterFragment.getStartOffset1() ||
beforeFragment.getEndOffset2() != afterFragment.getStartOffset2()) {
return false;
}
return true;
}
private static boolean isAdjoiningInner(@NotNull DiffFragment beforeFragment, @NotNull DiffFragment afterFragment,
int shift1, int shift2) {
if (beforeFragment.getEndOffset1() != afterFragment.getStartOffset1() + shift1 ||
beforeFragment.getEndOffset2() != afterFragment.getStartOffset2() + shift2) {
return false;
}
return true;
}
@NotNull
private static List<? extends DiffFragment> extractInnerFragments(@NotNull LineFragment lineFragment) {
if (lineFragment.getInnerFragments() != null) return lineFragment.getInnerFragments();
int length1 = lineFragment.getEndOffset1() - lineFragment.getStartOffset1();
int length2 = lineFragment.getEndOffset2() - lineFragment.getStartOffset2();
return Collections.singletonList(new DiffFragmentImpl(0, length1, 0, length2));
}
@NotNull
private static List<Line> getLines(@NotNull CharSequence text) {
List<Line> lines = new ArrayList<>();
int offset = 0;
while (true) {
int lineEnd = StringUtil.indexOf(text, '\n', offset);
if (lineEnd != -1) {
lines.add(new Line(text, offset, lineEnd, true));
offset = lineEnd + 1;
}
else {
lines.add(new Line(text, offset, text.length(), false));
break;
}
}
return lines;
}
private static class Line extends CharSequenceSubSequence {
private final int myOffset1;
private final int myOffset2;
private final boolean myNewline;
public Line(@NotNull CharSequence chars, int offset1, int offset2, boolean newline) {
super(chars, offset1, offset2);
myOffset1 = offset1;
myOffset2 = offset2;
myNewline = newline;
}
public int getOffset1() {
return myOffset1;
}
public int getOffset2() {
return myOffset2 + (myNewline ? 1 : 0);
}
}
}