blob: f1634953b5b360483dd4a84901461c7a5b075db9 [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 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.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.Pos;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/** Colors modified regions for {@link Unified}. */
class UnifiedChunkManager extends ChunkManager {
private static final JavaScriptObject focus = initOnClick();
private static final native JavaScriptObject initOnClick() /*-{
return $entry(function(e){
@com.google.gerrit.client.diff.UnifiedChunkManager::focus(
Lcom/google/gwt/dom/client/NativeEvent;)(e)
});
}-*/;
private List<UnifiedDiffChunkInfo> chunks;
@Override
DiffChunkInfo getFirst() {
return !chunks.isEmpty() ? chunks.get(0) : null;
}
private static void focus(NativeEvent event) {
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 Unified) {
((Unified) l).getCmFromSide(DisplaySide.A).focus();
event.stopPropagation();
}
}
}
static void focusOnClick(Element e) {
onClick(e, focus);
}
private final Unified host;
private final CodeMirror cm;
UnifiedChunkManager(Unified host,
CodeMirror cm,
Scrollbar scrollbar) {
super(scrollbar);
this.host = host;
this.cm = cm;
}
@Override
void render(DiffInfo diff) {
super.render();
LineMapper mapper = getLineMapper();
chunks = new ArrayList<>();
int cmLine = 0;
boolean useIntralineBg = diff.metaA() == null || diff.metaB() == null;
for (Region current : Natives.asList(diff.content())) {
int origLineA = mapper.getLineA();
int origLineB = mapper.getLineB();
if (current.ab() != null) {
int length = current.ab().length();
mapper.appendCommon(length);
for (int i = 0; i < length; i++) {
host.setLineNumber(DisplaySide.A, cmLine + i, origLineA + i + 1);
host.setLineNumber(DisplaySide.B, cmLine + i, origLineB + i + 1);
}
cmLine += length;
} else if (current.skip() > 0) {
mapper.appendCommon(current.skip());
cmLine += current.skip(); // Maybe current.ab().length();
} else if (current.common()) {
mapper.appendCommon(current.b().length());
cmLine += current.b().length();
} else {
cmLine += render(current, cmLine, useIntralineBg);
}
}
}
private int render(Region region, int cmLine, boolean useIntralineBg) {
LineMapper mapper = getLineMapper();
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;
boolean insertOrDelete = a == null || b == null;
colorLines(cm,
insertOrDelete && !useIntralineBg
? UnifiedTable.style.diffDelete()
: UnifiedTable.style.intralineDelete(), cmLine, aLen);
colorLines(cm,
insertOrDelete && !useIntralineBg
? UnifiedTable.style.diffInsert()
: UnifiedTable.style.intralineInsert(), cmLine + aLen,
bLen);
markEdit(DisplaySide.A, cmLine, a, region.editA());
markEdit(DisplaySide.B, cmLine + aLen, b, region.editB());
addGutterTag(region, cmLine); // TODO: verify addGutterTag
mapper.appendReplace(aLen, bLen);
int endA = mapper.getLineA() - 1;
int endB = mapper.getLineB() - 1;
if (aLen > 0) {
addDiffChunk(DisplaySide.A, endA, aLen, cmLine, bLen > 0);
for (int j = 0; j < aLen; j++) {
host.setLineNumber(DisplaySide.A, cmLine + j, startA + j + 1);
}
}
if (bLen > 0) {
addDiffChunk(DisplaySide.B, endB, bLen, cmLine + aLen, aLen > 0);
for (int j = 0; j < bLen; j++) {
host.setLineNumber(DisplaySide.B, cmLine + aLen + j, startB + j + 1);
}
}
return aLen + bLen;
}
private void addGutterTag(Region region, int cmLine) {
Scrollbar scrollbar = getScrollbar();
if (region.a() == null) {
scrollbar.insert(cm, cmLine, region.b().length());
} else if (region.b() == null) {
scrollbar.delete(cm, cm, cmLine, region.a().length());
} else {
scrollbar.edit(cm, cmLine, region.b().length());
}
}
private void markEdit(DisplaySide side, 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", getIntralineBgFromSide(side))
.set("readOnly", true);
Configuration diff = Configuration.create()
.set("className", getDiffColorFromSide(side))
.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,
getDiffColorFromSide(side),
from.line(), to.line());
}
}
private String getIntralineBgFromSide(DisplaySide side) {
return side == DisplaySide.A ? UnifiedTable.style.intralineDelete()
: UnifiedTable.style.intralineInsert();
}
private String getDiffColorFromSide(DisplaySide side) {
return side == DisplaySide.A ? UnifiedTable.style.diffDelete()
: UnifiedTable.style.diffInsert();
}
private void addDiffChunk(DisplaySide side, int chunkEnd, int chunkSize,
int cmLine, boolean edit) {
chunks.add(new UnifiedDiffChunkInfo(side, chunkEnd - chunkSize + 1, chunkEnd,
cmLine, 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 UnifiedDiffChunkInfo(cm.side(), 0, 0, line, false),
getDiffChunkComparatorCmLine());
diffChunkNavHelper(chunks, cm, res, dir);
}
};
}
/** Diff chunks are ordered by their starting lines in CodeMirror */
private Comparator<UnifiedDiffChunkInfo> getDiffChunkComparatorCmLine() {
return new Comparator<UnifiedDiffChunkInfo>() {
@Override
public int compare(UnifiedDiffChunkInfo o1, UnifiedDiffChunkInfo o2) {
return o1.getCmLine() - o2.getCmLine();
}
};
}
@Override
int getCmLine(int line, DisplaySide side) {
int res =
Collections.binarySearch(chunks,
new UnifiedDiffChunkInfo(
side, line, 0, 0, false), // Dummy DiffChunkInfo
getDiffChunkComparator());
if (res >= 0) {
return chunks.get(res).getCmLine();
} else { // The line might be within a DiffChunk
res = -res - 1;
if (res > 0) {
UnifiedDiffChunkInfo info = chunks.get(res - 1);
if (info.getSide() == side && info.getStart() <= line && line <= info.getEnd()) {
return info.getCmLine() + line - info.getStart();
} else {
return info.getCmLine() + (side == info.getSide()
? line
: getLineMapper().lineOnOther(side, line).getLine()) - info.getStart();
}
} else {
return line;
}
}
}
LineSidePair getLineSidePairFromCmLine(int cmLine) {
int res =
Collections.binarySearch(chunks,
new UnifiedDiffChunkInfo(
DisplaySide.A, 0, 0, cmLine, false), // Dummy DiffChunkInfo
getDiffChunkComparatorCmLine());
if (res >= 0) {
UnifiedDiffChunkInfo info = chunks.get(res);
return new LineSidePair(info.getStart(), info.getSide());
} else { // The line might be within a DiffChunk
res = -res - 1;
if (res > 0) {
UnifiedDiffChunkInfo info = chunks.get(res - 1);
int delta = info.getCmLine() - 1 + info.getEnd() - info.getStart() - cmLine;
if (delta > 0) {
return new LineSidePair(info.getStart() + delta, info.getSide());
} else {
delta = cmLine - info.getCmLine();
int result = info.getStart() + delta;
return new LineSidePair(result, info.getSide());
}
} else {
// Always return side B
return new LineSidePair(cmLine, DisplaySide.B);
}
}
}
static class LineSidePair {
private int line;
private DisplaySide side;
LineSidePair(int line, DisplaySide side) {
this.line = line;
this.side = side;
}
int getLine() {
return line;
}
DisplaySide getSide() {
return side;
}
}
}