// Copyright (C) 2013 The Android Open Source Project // // 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.google.gerrit.client.diff; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** Helper class to handle calculations involving line gaps. */ class LineMapper { private int lineA; private int lineB; private List<LineGap> lineMapAtoB; private List<LineGap> lineMapBtoA; LineMapper() { reset(); } void reset() { lineA = 0; lineB = 0; lineMapAtoB = new ArrayList<>(); lineMapBtoA = new ArrayList<>(); } int getLineA() { return lineA; } int getLineB() { return lineB; } void appendCommon(int numLines) { lineA += numLines; lineB += numLines; } void appendReplace(int aLen, int bLen) { appendCommon(Math.min(aLen, bLen)); if (aLen < bLen) { // Edit with insertion appendInsert(bLen - aLen); } else if (aLen > bLen) { // Edit with deletion appendDelete(aLen - bLen); } } void appendInsert(int numLines) { int origLineB = lineB; lineB += numLines; int bAheadOfA = lineB - lineA; lineMapAtoB.add(new LineGap(lineA, -1, bAheadOfA)); lineMapBtoA.add(new LineGap(origLineB, lineB - 1, -bAheadOfA)); } void appendDelete(int numLines) { int origLineA = lineA; lineA += numLines; int aAheadOfB = lineA - lineB; lineMapAtoB.add(new LineGap(origLineA, lineA - 1, -aAheadOfB)); lineMapBtoA.add(new LineGap(lineB, -1, aAheadOfB)); } /** * Helper method to retrieve the line number on the other side. * * Given a line number on one side, performs a binary search in the lineMap * to find the corresponding LineGap record. * * A LineGap records gap information from the start of an actual gap up to * the start of the next gap. In the following example, * lineMapAtoB will have LineGap: {start: 1, end: -1, delta: 3} * (end set to -1 to represent a dummy gap of length zero. The binary search * only looks at start so setting it to -1 has no effect here.) * lineMapBtoA will have LineGap: {start: 1, end: 3, delta: -3} * These LineGaps control lines between 1 and 5. * * The "delta" is computed as the number to add on our side to get the line * number on the other side given a line after the actual gap, so the result * will be (line + delta). All lines within the actual gap (1 to 3) are * considered corresponding to the last line above the region on the other * side, which is 0 in this case. For these lines, we do (end + delta). * * For example, to get the line number on the left corresponding to 1 on the * right (lineOnOther(REVISION, 1)), the method looks up in lineMapBtoA, * finds the "delta" to be -3, and returns 3 + (-3) = 0 since 1 falls in the * actual gap. On the other hand, the line corresponding to 5 on the right * will be 5 + (-3) = 2, since 5 is in the region after the gap (but still * controlled by the current LineGap). * * PARENT REVISION * 0 | 0 * - | 1 \ \ * - | 2 | Actual insertion gap | * - | 3 / | Region controlled by one LineGap * 1 | 4 <- delta = 4 - 1 = 3 | * 2 | 5 / * - | 6 * ... */ LineOnOtherInfo lineOnOther(DisplaySide mySide, int line) { List<LineGap> lineGaps = gapList(mySide); // Create a dummy LineGap for the search. int ret = Collections.binarySearch(lineGaps, new LineGap(line)); if (ret == -1) { return new LineOnOtherInfo(line, true); } else { LineGap lookup = lineGaps.get(0 <= ret ? ret : -ret - 2); int start = lookup.start; int end = lookup.end; int delta = lookup.delta; if (start <= line && line <= end && end != -1) { // Line falls within gap return new LineOnOtherInfo(end + delta, false); } else { // Line after gap return new LineOnOtherInfo(line + delta, true); } } } AlignedPair align(DisplaySide mySide, int line) { List<LineGap> gaps = gapList(mySide); int idx = Collections.binarySearch(gaps, new LineGap(line)); if (idx == -1) { return new AlignedPair(line, line); } LineGap g = gaps.get(0 <= idx ? idx : -idx - 2); if (g.start <= line && line <= g.end && g.end != -1) { if (0 < g.start) { // Line falls within this gap, use alignment before. return new AlignedPair(g.start - 1, g.end + g.delta); } return new AlignedPair(g.end, g.end + g.delta + 1); } return new AlignedPair(line, line + g.delta); } private List<LineGap> gapList(DisplaySide mySide) { return mySide == DisplaySide.A ? lineMapAtoB : lineMapBtoA; } static class AlignedPair { final int src; final int dst; AlignedPair(int s, int d) { src = s; dst = d; } } /** * @field line The line number on the other side. * @field aligned Whether the two lines are at the same height when displayed. */ static class LineOnOtherInfo { private int line; private boolean aligned; LineOnOtherInfo(int line, boolean aligned) { this.line = line; this.aligned = aligned; } int getLine() { return line; } boolean isAligned() { return aligned; } @Override public boolean equals(Object obj) { if (obj instanceof LineOnOtherInfo) { LineOnOtherInfo other = (LineOnOtherInfo) obj; return aligned == other.aligned && line == other.line; } return false; } @Override public String toString() { return line + " " + aligned; } } /** * Helper class to record line gap info and assist in calculation of line * number on the other side. * * For a mapping from A to B, where A is the side with an insertion: * @field start The start line of the insertion in A. * @field end The exclusive end line of the insertion in A. * @field delta The offset added to A to get the line number in B calculated * from end. */ private static class LineGap implements Comparable<LineGap> { private final int start; private final int end; private final int delta; private LineGap(int start, int end, int delta) { this.start = start; this.end = end; this.delta = delta; } private LineGap(int line) { this(line, 0, 0); } @Override public int compareTo(LineGap o) { return start - o.start; } } }