blob: 11b7e7c580e84fc075e63c5a029be4a93307cab4 [file] [log] [blame]
// 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.DisplaySide.A;
import static com.google.gerrit.client.diff.DisplaySide.B;
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.JavaScriptObject;
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.NativeEvent;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.EventListener;
import net.codemirror.lib.CodeMirror;
import net.codemirror.lib.CodeMirror.LineClassWhere;
import net.codemirror.lib.Configuration;
import net.codemirror.lib.LineWidget;
import net.codemirror.lib.Pos;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** Colors modified regions for {@link SideBySide}. */
class SideBySideChunkManager extends ChunkManager {
private static final String DATA_LINES = "_cs2h";
private static double guessedLineHeightPx = 15;
private static final JavaScriptObject focusA = initOnClick(A);
private static final JavaScriptObject focusB = initOnClick(B);
private static native JavaScriptObject initOnClick(DisplaySide s) /*-{
return $entry(function(e){
@com.google.gerrit.client.diff.SideBySideChunkManager::focus(
Lcom/google/gwt/dom/client/NativeEvent;
Lcom/google/gerrit/client/diff/DisplaySide;)(e,s)
});
}-*/;
private static void focus(NativeEvent event, DisplaySide side) {
Element e = Element.as(event.getEventTarget());
for (e = DOM.getParent(e); e != null; e = DOM.getParent(e)) {
EventListener l = DOM.getEventListener(e);
if (l instanceof SideBySide) {
((SideBySide) l).getCmFromSide(side).focus();
event.stopPropagation();
}
}
}
static void focusOnClick(Element e, DisplaySide side) {
onClick(e, side == A ? focusA : focusB);
}
private final SideBySide host;
private final CodeMirror cmA;
private final CodeMirror cmB;
private List<DiffChunkInfo> chunks;
private List<LineWidget> padding;
private List<Element> paddingDivs;
SideBySideChunkManager(SideBySide host,
CodeMirror cmA,
CodeMirror cmB,
Scrollbar scrollbar) {
super(scrollbar);
this.host = host;
this.cmA = cmA;
this.cmB = cmB;
}
@Override
DiffChunkInfo getFirst() {
return !chunks.isEmpty() ? chunks.get(0) : null;
}
@Override
void reset() {
super.reset();
for (LineWidget w : padding) {
w.clear();
}
}
@Override
void render(DiffInfo diff) {
super.render();
chunks = new ArrayList<>();
padding = new ArrayList<>();
paddingDivs = new ArrayList<>();
String diffColor = diff.metaA() == null || diff.metaB() == null
? SideBySideTable.style.intralineBg()
: SideBySideTable.style.diff();
for (Region current : Natives.asList(diff.content())) {
if (current.ab() != null) {
lineMapper.appendCommon(current.ab().length());
} else if (current.skip() > 0) {
lineMapper.appendCommon(current.skip());
} else if (current.common()) {
lineMapper.appendCommon(current.b().length());
} else {
render(current, diffColor);
}
}
if (paddingDivs.isEmpty()) {
paddingDivs = null;
}
}
void adjustPadding() {
if (paddingDivs != null) {
double h = cmB.extras().lineHeightPx();
for (Element div : paddingDivs) {
int lines = div.getPropertyInt(DATA_LINES);
div.getStyle().setHeight(lines * h, Unit.PX);
}
for (LineWidget w : padding) {
w.changed();
}
paddingDivs = null;
guessedLineHeightPx = h;
}
}
private void render(Region region, String diffColor) {
int startA = lineMapper.getLineA();
int startB = lineMapper.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
: SideBySideTable.style.intralineBg();
colorLines(cmA, color, startA, aLen);
colorLines(cmB, color, startB, bLen);
markEdit(cmA, startA, a, region.editA());
markEdit(cmB, startB, b, region.editB());
addPadding(cmA, startA + aLen - 1, bLen - aLen);
addPadding(cmB, startB + bLen - 1, aLen - bLen);
addGutterTag(region, startA, startB);
lineMapper.appendReplace(aLen, bLen);
int endA = lineMapper.getLineA() - 1;
int endB = lineMapper.getLineB() - 1;
if (aLen > 0) {
addDiffChunk(cmB, endA, aLen, bLen > 0);
}
if (bLen > 0) {
addDiffChunk(cmA, endB, bLen, aLen > 0);
}
}
private void addGutterTag(Region region, int startA, int startB) {
if (region.a() == null) {
scrollbar.insert(cmB, startB, region.b().length());
} else if (region.b() == null) {
scrollbar.delete(cmA, cmB, startA, region.a().length());
} else {
scrollbar.edit(cmB, startB, region.b().length());
}
}
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", SideBySideTable.style.intralineBg())
.set("readOnly", true);
Configuration diff = Configuration.create()
.set("className", SideBySideTable.style.diff())
.set("readOnly", true);
Pos last = Pos.create(0, 0);
for (Span span : Natives.asList(edits)) {
Pos from = iter.advance(span.skip());
Pos to = iter.advance(span.mark());
if (from.line() == last.line()) {
getMarkers().add(cm.markText(last, from, bg));
} else {
getMarkers().add(cm.markText(Pos.create(from.line(), 0), from, bg));
}
getMarkers().add(cm.markText(from, to, diff));
last = to;
colorLines(cm, LineClassWhere.BACKGROUND,
SideBySideTable.style.diff(),
from.line(), to.line());
}
}
/**
* 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, final int len) {
if (0 < len) {
Element pad = DOM.createDiv();
pad.setClassName(SideBySideTable.style.padding());
pad.setPropertyInt(DATA_LINES, len);
pad.getStyle().setHeight(guessedLineHeightPx * len, Unit.PX);
focusOnClick(pad, cm.side());
paddingDivs.add(pad);
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 lineOnOther,
int chunkSize, boolean edit) {
chunks.add(new DiffChunkInfo(host.otherCm(cmToPad).side(),
lineOnOther - chunkSize + 1, lineOnOther, edit));
}
@Override
Runnable diffChunkNav(final CodeMirror cm, final Direction dir) {
return new Runnable() {
@Override
public void run() {
int line = cm.extras().hasActiveLine()
? cm.getLineNumber(cm.extras().activeLine())
: 0;
int res = Collections.binarySearch(
chunks,
new DiffChunkInfo(cm.side(), line, 0, false),
getDiffChunkComparator());
diffChunkNavHelper(chunks, host, res, dir);
}
};
}
@Override
int getCmLine(int line, DisplaySide side) {
return line;
}
}