blob: 98ad023c9dfece01bf57d6dfca6466a782886864 [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 java.util.Comparator.comparing;
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 java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import net.codemirror.lib.CodeMirror;
import net.codemirror.lib.CodeMirror.LineClassWhere;
import net.codemirror.lib.Configuration;
import net.codemirror.lib.Pos;
/** Colors modified regions for {@link Unified}. */
class UnifiedChunkManager extends ChunkManager {
private static final JavaScriptObject focus = initOnClick();
private static 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();
chunks = new ArrayList<>();
int cmLine = 0;
boolean useIntralineBg = diff.metaA() == null || diff.metaB() == null;
for (Region current : Natives.asList(diff.content())) {
int origLineA = lineMapper.getLineA();
int origLineB = lineMapper.getLineB();
if (current.ab() != null) {
int length = current.ab().length();
lineMapper.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) {
lineMapper.appendCommon(current.skip());
cmLine += current.skip(); // Maybe current.ab().length();
} else if (current.common()) {
lineMapper.appendCommon(current.b().length());
cmLine += current.b().length();
} else {
cmLine += render(current, cmLine, useIntralineBg);
}
}
host.setLineNumber(DisplaySide.A, cmLine, lineMapper.getLineA() + 1);
host.setLineNumber(DisplaySide.B, cmLine, lineMapper.getLineB() + 1);
}
private int render(Region region, int cmLine, boolean useIntralineBg) {
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;
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
lineMapper.appendReplace(aLen, bLen);
int endA = lineMapper.getLineA() - 1;
int endB = lineMapper.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);
host.setLineNumberEmpty(DisplaySide.B, cmLine + j);
}
}
if (bLen > 0) {
addDiffChunk(DisplaySide.B, endB, bLen, cmLine + aLen, aLen > 0);
for (int j = 0; j < bLen; j++) {
host.setLineNumberEmpty(DisplaySide.A, cmLine + aLen + j);
host.setLineNumber(DisplaySide.B, cmLine + aLen + j, startB + j + 1);
}
}
return aLen + bLen;
}
private void addGutterTag(Region region, int cmLine) {
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(CodeMirror cm, Direction dir) {
return () -> {
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, host, res, dir);
};
}
/** Diff chunks are ordered by their starting lines in CodeMirror */
private Comparator<UnifiedDiffChunkInfo> getDiffChunkComparatorCmLine() {
return comparing(UnifiedDiffChunkInfo::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();
}
// The line might be within a DiffChunk
res = -res - 1;
if (res > 0) {
UnifiedDiffChunkInfo info = chunks.get(res - 1);
if (side == DisplaySide.A && info.isEdit() && info.getSide() == DisplaySide.B) {
// Need to use the start and cmLine of the deletion chunk
UnifiedDiffChunkInfo delete = chunks.get(res - 2);
if (line <= delete.getEnd()) {
return delete.getCmLine() + line - delete.getStart();
}
// Need to add the length of the insertion chunk
return delete.getCmLine() + line - delete.getStart() + info.getEnd() - info.getStart() + 1;
} else if (side == info.getSide()) {
return info.getCmLine() + line - info.getStart();
} else {
return info.getCmLine() + lineMapper.lineOnOther(side, line).getLine() - info.getStart();
}
}
return line;
}
LineRegionInfo getLineRegionInfoFromCmLine(int cmLine) {
int res =
Collections.binarySearch(
chunks,
new UnifiedDiffChunkInfo(DisplaySide.A, 0, 0, cmLine, false), // Dummy DiffChunkInfo
getDiffChunkComparatorCmLine());
if (res >= 0) { // The line is right at the start of a diff chunk.
UnifiedDiffChunkInfo info = chunks.get(res);
return new LineRegionInfo(info.getStart(), displaySideToRegionType(info.getSide()));
}
// The line might be within or after a diff chunk.
res = -res - 1;
if (res > 0) {
UnifiedDiffChunkInfo info = chunks.get(res - 1);
int lineOnInfoSide = info.getStart() + cmLine - info.getCmLine();
if (lineOnInfoSide > info.getEnd()) { // After a diff chunk
if (info.getSide() == DisplaySide.A) {
// For the common region after a deletion chunk, associate the line
// on side B with a common region.
return new LineRegionInfo(
lineMapper.lineOnOther(DisplaySide.A, lineOnInfoSide).getLine(), RegionType.COMMON);
}
return new LineRegionInfo(lineOnInfoSide, RegionType.COMMON);
}
// Within a diff chunk
return new LineRegionInfo(lineOnInfoSide, displaySideToRegionType(info.getSide()));
}
// The line is before any diff chunk, so it always equals cmLine and
// belongs to a common region.
return new LineRegionInfo(cmLine, RegionType.COMMON);
}
enum RegionType {
INSERT,
DELETE,
COMMON,
}
private static RegionType displaySideToRegionType(DisplaySide side) {
return side == DisplaySide.A ? RegionType.DELETE : RegionType.INSERT;
}
/**
* Helper class to associate a line in the original file with the type of the region it belongs
* to.
*
* @field line The 0-based line number in the original file. Note that this might be different
* from the line number shown in CodeMirror.
* @field type The type of the region the line belongs to. Can be INSERT, DELETE or COMMON.
*/
static class LineRegionInfo {
final int line;
final RegionType type;
LineRegionInfo(int line, RegionType type) {
this.line = line;
this.type = type;
}
DisplaySide getSide() {
// Always return DisplaySide.B for INSERT or COMMON
return type == RegionType.DELETE ? DisplaySide.A : DisplaySide.B;
}
}
}