| // 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 static com.google.gerrit.client.diff.OverviewBar.MarkType.DELETE; |
| import static com.google.gerrit.client.diff.OverviewBar.MarkType.EDIT; |
| import static com.google.gerrit.client.diff.OverviewBar.MarkType.INSERT; |
| |
| import com.google.gerrit.client.diff.DiffInfo.Region; |
| import com.google.gerrit.client.diff.DiffInfo.Span; |
| import com.google.gerrit.client.rpc.Natives; |
| import com.google.gwt.core.client.JsArray; |
| import com.google.gwt.core.client.JsArrayString; |
| import com.google.gwt.dom.client.Element; |
| import com.google.gwt.dom.client.Style.Unit; |
| import com.google.gwt.user.client.DOM; |
| |
| import net.codemirror.lib.CodeMirror; |
| import net.codemirror.lib.CodeMirror.LineClassWhere; |
| import net.codemirror.lib.Configuration; |
| import net.codemirror.lib.LineCharacter; |
| import net.codemirror.lib.LineWidget; |
| import net.codemirror.lib.TextMarker; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.List; |
| |
| /** Colors modified regions for {@link SideBySide2}. */ |
| class ChunkManager { |
| private final SideBySide2 host; |
| private final CodeMirror cmA; |
| private final CodeMirror cmB; |
| private final OverviewBar sidePanel; |
| private final LineMapper mapper; |
| |
| private List<DiffChunkInfo> chunks; |
| private List<TextMarker> markers; |
| private List<Runnable> undo; |
| private List<LineWidget> padding; |
| |
| ChunkManager(SideBySide2 host, |
| CodeMirror cmA, |
| CodeMirror cmB, |
| OverviewBar sidePanel) { |
| this.host = host; |
| this.cmA = cmA; |
| this.cmB = cmB; |
| this.sidePanel = sidePanel; |
| this.mapper = new LineMapper(); |
| } |
| |
| LineMapper getLineMapper() { |
| return mapper; |
| } |
| |
| DiffChunkInfo getFirst() { |
| if (!chunks.isEmpty()) { |
| for (DiffChunkInfo d : chunks) { |
| if (d.getSide() == DisplaySide.B) { |
| return d; |
| } |
| } |
| return chunks.get(0); |
| } |
| return null; |
| } |
| |
| void reset() { |
| mapper.reset(); |
| for (TextMarker m : markers) { |
| m.clear(); |
| } |
| for (Runnable r : undo) { |
| r.run(); |
| } |
| for (LineWidget w : padding) { |
| w.clear(); |
| } |
| } |
| |
| void render(DiffInfo diff) { |
| chunks = new ArrayList<>(); |
| markers = new ArrayList<>(); |
| undo = new ArrayList<>(); |
| padding = new ArrayList<>(); |
| |
| String diffColor = diff.meta_a() == null || diff.meta_b() == null |
| ? DiffTable.style.intralineBg() |
| : DiffTable.style.diff(); |
| |
| for (Region current : Natives.asList(diff.content())) { |
| if (current.ab() != null) { |
| mapper.appendCommon(current.ab().length()); |
| } else if (current.skip() > 0) { |
| mapper.appendCommon(current.skip()); |
| } else if (current.common()) { |
| mapper.appendCommon(current.b().length()); |
| } else { |
| render(current, diffColor); |
| } |
| } |
| } |
| |
| private void render(Region region, String diffColor) { |
| int startA = mapper.getLineA(); |
| int startB = mapper.getLineB(); |
| |
| JsArrayString a = region.a(); |
| JsArrayString b = region.b(); |
| int aLen = a != null ? a.length() : 0; |
| int bLen = b != null ? b.length() : 0; |
| |
| String color = a == null || b == null |
| ? diffColor |
| : DiffTable.style.intralineBg(); |
| |
| colorLines(cmA, color, startA, aLen); |
| colorLines(cmB, color, startB, bLen); |
| markEdit(cmA, startA, a, region.edit_a()); |
| markEdit(cmB, startB, b, region.edit_b()); |
| addPadding(cmA, startA + aLen - 1, bLen - aLen); |
| addPadding(cmB, startB + bLen - 1, aLen - bLen); |
| addGutterTag(region, startA, startB); |
| mapper.appendReplace(aLen, bLen); |
| |
| int endA = mapper.getLineA() - 1; |
| int endB = mapper.getLineB() - 1; |
| if (aLen > 0) { |
| addDiffChunk(cmB, endB, endA, aLen, bLen > 0); |
| } |
| if (bLen > 0) { |
| addDiffChunk(cmA, endA, endB, bLen, aLen > 0); |
| } |
| } |
| |
| private void addGutterTag(Region region, int startA, int startB) { |
| if (region.a() == null) { |
| sidePanel.add(cmB, startB, region.b().length(), INSERT); |
| } else if (region.b() == null) { |
| sidePanel.add(cmA, startA, region.a().length(), DELETE); |
| } else { |
| sidePanel.add(cmB, startB, region.b().length(), EDIT); |
| } |
| } |
| |
| private void markEdit(CodeMirror cm, int startLine, |
| JsArrayString lines, JsArray<Span> edits) { |
| if (lines == null || edits == null) { |
| return; |
| } |
| |
| EditIterator iter = new EditIterator(lines, startLine); |
| Configuration bg = Configuration.create() |
| .set("className", DiffTable.style.intralineBg()) |
| .set("readOnly", true); |
| |
| Configuration diff = Configuration.create() |
| .set("className", DiffTable.style.diff()) |
| .set("readOnly", true); |
| |
| LineCharacter last = CodeMirror.pos(0, 0); |
| for (Span span : Natives.asList(edits)) { |
| LineCharacter from = iter.advance(span.skip()); |
| LineCharacter to = iter.advance(span.mark()); |
| if (from.getLine() == last.getLine()) { |
| markers.add(cm.markText(last, from, bg)); |
| } else { |
| markers.add(cm.markText(CodeMirror.pos(from.getLine(), 0), from, bg)); |
| } |
| markers.add(cm.markText(from, to, diff)); |
| last = to; |
| colorLines(cm, LineClassWhere.BACKGROUND, |
| DiffTable.style.diff(), |
| from.getLine(), to.getLine()); |
| } |
| } |
| |
| private void colorLines(CodeMirror cm, String color, int line, int cnt) { |
| colorLines(cm, LineClassWhere.WRAP, color, line, line + cnt); |
| } |
| |
| private void colorLines(final CodeMirror cm, final LineClassWhere where, |
| final String className, final int start, final int end) { |
| if (start < end) { |
| for (int line = start; line < end; line++) { |
| cm.addLineClass(line, where, className); |
| } |
| undo.add(new Runnable() { |
| @Override |
| public void run() { |
| for (int line = start; line < end; line++) { |
| cm.removeLineClass(line, where, className); |
| } |
| } |
| }); |
| } |
| } |
| |
| /** |
| * Insert a new padding div below the given line. |
| * |
| * @param cm parent CodeMirror to add extra space into. |
| * @param line line to put the padding below. |
| * @param len number of lines to pad. Padding is inserted only if |
| * {@code len >= 1}. |
| */ |
| private void addPadding(CodeMirror cm, int line, int len) { |
| if (0 < len) { |
| // DiffTable adds 1px bottom padding to each line to preserve |
| // sufficient space for underscores commonly appearing in code. |
| // Padding should be 1em + 1px high for each line. Add within |
| // the browser using height + padding-bottom. |
| Element pad = DOM.createDiv(); |
| pad.setClassName(DiffTable.style.padding()); |
| pad.getStyle().setHeight(len, Unit.EM); |
| pad.getStyle().setPaddingBottom(len, Unit.PX); |
| padding.add(cm.addLineWidget( |
| line == -1 ? 0 : line, |
| pad, |
| Configuration.create() |
| .set("coverGutter", true) |
| .set("noHScroll", true) |
| .set("above", line == -1))); |
| } |
| } |
| |
| private void addDiffChunk(CodeMirror cmToPad, int lineToPad, |
| int lineOnOther, int chunkSize, boolean edit) { |
| chunks.add(new DiffChunkInfo(host.otherCm(cmToPad).side(), |
| lineOnOther - chunkSize + 1, lineOnOther, edit)); |
| } |
| |
| Runnable diffChunkNav(final CodeMirror cm, final Direction dir) { |
| return new Runnable() { |
| @Override |
| public void run() { |
| int line = cm.hasActiveLine() ? cm.getLineNumber(cm.getActiveLine()) : 0; |
| int res = Collections.binarySearch( |
| chunks, |
| new DiffChunkInfo(cm.side(), line, 0, false), |
| getDiffChunkComparator()); |
| if (res < 0) { |
| res = -res - (dir == Direction.PREV ? 1 : 2); |
| } |
| res = res + (dir == Direction.PREV ? -1 : 1); |
| if (res < 0 || chunks.size() <= res) { |
| return; |
| } |
| |
| DiffChunkInfo lookUp = chunks.get(res); |
| // If edit, skip the deletion chunk and set focus on the insertion one. |
| if (lookUp.isEdit() && lookUp.getSide() == DisplaySide.A) { |
| res = res + (dir == Direction.PREV ? -1 : 1); |
| if (res < 0 || chunks.size() <= res) { |
| return; |
| } |
| } |
| |
| DiffChunkInfo target = chunks.get(res); |
| CodeMirror targetCm = host.getCmFromSide(target.getSide()); |
| targetCm.setCursor(LineCharacter.create(target.getStart())); |
| targetCm.focus(); |
| targetCm.scrollToY( |
| targetCm.heightAtLine(target.getStart(), "local") - |
| 0.5 * cmB.getScrollbarV().getClientHeight()); |
| } |
| }; |
| } |
| |
| private Comparator<DiffChunkInfo> getDiffChunkComparator() { |
| // Chunks are ordered by their starting line. If it's a deletion, |
| // use its corresponding line on the revision side for comparison. |
| // In the edit case, put the deletion chunk right before the |
| // insertion chunk. This placement guarantees well-ordering. |
| return new Comparator<DiffChunkInfo>() { |
| @Override |
| public int compare(DiffChunkInfo a, DiffChunkInfo b) { |
| if (a.getSide() == b.getSide()) { |
| return a.getStart() - b.getStart(); |
| } else if (a.getSide() == DisplaySide.A) { |
| int comp = mapper.lineOnOther(a.getSide(), a.getStart()) |
| .getLine() - b.getStart(); |
| return comp == 0 ? -1 : comp; |
| } else { |
| int comp = a.getStart() - |
| mapper.lineOnOther(b.getSide(), b.getStart()).getLine(); |
| return comp == 0 ? 1 : comp; |
| } |
| } |
| }; |
| } |
| |
| DiffChunkInfo getDiffChunk(DisplaySide side, int line) { |
| int res = Collections.binarySearch( |
| chunks, |
| new DiffChunkInfo(side, line, 0, false), // Dummy DiffChunkInfo |
| getDiffChunkComparator()); |
| if (res >= 0) { |
| return chunks.get(res); |
| } else { // The line might be within a DiffChunk |
| res = -res - 1; |
| if (res > 0) { |
| DiffChunkInfo info = chunks.get(res - 1); |
| if (info.getSide() == side && info.getStart() <= line && |
| line <= info.getEnd()) { |
| return info; |
| } |
| } |
| } |
| return null; |
| } |
| } |