/*
* 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.ByWord.InlineChunk;
import com.intellij.diff.comparison.ByWord.NewlineChunk;
import com.intellij.diff.comparison.iterables.FairDiffIterable;
import com.intellij.diff.util.Range;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
/*
* Given matchings on words, split initial line block into 'logically different' line blocks
*/
class LineFragmentSplitter {
@NotNull private final CharSequence myText1;
@NotNull private final CharSequence myText2;
@NotNull private final List<InlineChunk> myWords1;
@NotNull private final List<InlineChunk> myWords2;
@NotNull private final FairDiffIterable myIterable;
@NotNull private final ProgressIndicator myIndicator;
@NotNull private final List<WordBlock> myResult = new ArrayList<WordBlock>();
public LineFragmentSplitter(@NotNull CharSequence text1,
@NotNull CharSequence text2,
@NotNull List<InlineChunk> words1,
@NotNull List<InlineChunk> words2,
@NotNull FairDiffIterable iterable,
@NotNull ProgressIndicator indicator) {
myText1 = text1;
myText2 = text2;
myWords1 = words1;
myWords2 = words2;
myIterable = iterable;
myIndicator = indicator;
}
private int last1 = -1;
private int last2 = -1;
private boolean lastHasEqualWords = false;
private boolean hasEqualWords = false;
// indexes here are a bit tricky
// -1 - the beginning of file, words.size() - end of file, everything in between - InlineChunks (words or newlines)
@NotNull
public List<WordBlock> run() {
for (Range range : myIterable.iterateUnchanged()) {
int count = range.end1 - range.start1;
for (int i = 0; i < count; i++) {
int index1 = range.start1 + i;
int index2 = range.start2 + i;
if (isNewline(myWords1, index1) && isNewline(myWords2, index2)) { // split by matched newlines
addLineChunk(index1, index2);
}
else {
if (isFirstInLine(myWords1, index1) && isFirstInLine(myWords2, index2)) { // split by matched first word in line
addLineChunk(index1 - 1, index2 - 1);
}
// TODO: split by 'last word in line' + 'last word in whole sequence' ?
hasEqualWords = true;
}
}
}
addLineChunk(myWords1.size(), myWords2.size());
return myResult;
}
private void addLineChunk(int end1, int end2) {
if (last1 > end1 || last2 > end2) return;
WordBlock block = createBlock(last1, last2, end1, end2);
if (block.offsets.isEmpty()) return;
WordBlock lastBlock = ContainerUtil.getLastItem(myResult);
if (lastBlock != null && shouldMergeBlocks(lastBlock, block)) {
myResult.remove(myResult.size() - 1);
myResult.add(mergeBlocks(lastBlock, block));
lastHasEqualWords = hasEqualWords || lastHasEqualWords;
}
else {
myResult.add(block);
lastHasEqualWords = hasEqualWords;
}
hasEqualWords = false;
last1 = end1;
last2 = end2;
}
@NotNull
private WordBlock createBlock(int start1, int start2, int end1, int end2) {
int startOffset1 = getOffset(myWords1, myText1, start1);
int startOffset2 = getOffset(myWords2, myText2, start2);
int endOffset1 = getOffset(myWords1, myText1, end1);
int endOffset2 = getOffset(myWords2, myText2, end2);
start1 = Math.max(0, start1 + 1);
start2 = Math.max(0, start2 + 1);
end1 = Math.min(end1 + 1, myWords1.size());
end2 = Math.min(end2 + 1, myWords2.size());
return new WordBlock(new Range(start1, end1, start2, end2), new Range(startOffset1, endOffset1, startOffset2, endOffset2));
}
private boolean shouldMergeBlocks(@NotNull WordBlock lastBlock, @NotNull WordBlock newBlock) {
if (!lastHasEqualWords && !hasEqualWords) return true; // combine lines, that matched only by '\n'
if (isEqualsIgnoreWhitespace(newBlock) && isEqualsIgnoreWhitespace(lastBlock)) return true; // combine whitespace-only changed lines
if (noWordsInside(lastBlock) || noWordsInside(newBlock)) return true; // squash block without words in it
return false;
}
private boolean isEqualsIgnoreWhitespace(@NotNull WordBlock block) {
CharSequence sequence1 = myText1.subSequence(block.offsets.start1, block.offsets.end1);
CharSequence sequence2 = myText2.subSequence(block.offsets.start2, block.offsets.end2);
return StringUtil.equalsIgnoreWhitespaces(sequence1, sequence2);
}
@NotNull
private static WordBlock mergeBlocks(@NotNull WordBlock start, @NotNull WordBlock end) {
return new WordBlock(new Range(start.words.start1, end.words.end1, start.words.start2, end.words.end2),
new Range(start.offsets.start1, end.offsets.end1, start.offsets.start2, end.offsets.end2));
}
private static int getOffset(@NotNull List<InlineChunk> words, @NotNull CharSequence text, int index) {
if (index == -1) return 0;
if (index == words.size()) return text.length();
InlineChunk chunk = words.get(index);
assert chunk instanceof NewlineChunk;
return chunk.getOffset2();
}
private static boolean isNewline(@NotNull List<InlineChunk> words1, int index) {
return words1.get(index) instanceof NewlineChunk;
}
private static boolean isFirstInLine(@NotNull List<InlineChunk> words1, int index) {
if (index == 0) return true;
return words1.get(index - 1) instanceof NewlineChunk;
}
private boolean noWordsInside(@NotNull WordBlock block) {
for (int i = block.words.start1; i < block.words.end1; i++) {
if (!(myWords1.get(i) instanceof NewlineChunk)) return false;
}
for (int i = block.words.start2; i < block.words.end2; i++) {
if (!(myWords2.get(i) instanceof NewlineChunk)) return false;
}
return true;
}
//
// Helpers
//
public static class WordBlock {
@NotNull public final Range words;
@NotNull public final Range offsets;
public WordBlock(@NotNull Range words, @NotNull Range offsets) {
this.words = words;
this.offsets = offsets;
}
}
}