Merge "New unified diff based on CodeMirror"

This commit is contained in:
Dave Borowitz
2016-03-15 08:46:34 +00:00
committed by Gerrit Code Review
42 changed files with 4372 additions and 1838 deletions

View File

@@ -83,6 +83,7 @@ import com.google.gerrit.client.dashboards.DashboardInfo;
import com.google.gerrit.client.dashboards.DashboardList;
import com.google.gerrit.client.diff.DisplaySide;
import com.google.gerrit.client.diff.SideBySide;
import com.google.gerrit.client.diff.Unified;
import com.google.gerrit.client.documentation.DocScreen;
import com.google.gerrit.client.editor.EditScreen;
import com.google.gerrit.client.groups.GroupApi;
@@ -471,14 +472,16 @@ public class Dispatcher {
if ("".equals(panel) || /* DEPRECATED URL */"cm".equals(panel)) {
if (preferUnified()) {
unified(token, baseId, id);
unified(token, baseId, id, side, line);
} else {
codemirror(token, baseId, id, side, line, false);
}
} else if ("sidebyside".equals(panel)) {
codemirror(token, null, id, side, line, false);
} else if ("unified".equals(panel)) {
unified(token, baseId, id);
unified(token, baseId, id, side, line);
} else if ("unified1".equals(panel)) {
unified1(token, baseId, id);
} else if ("edit".equals(panel)) {
codemirror(token, null, id, side, line, true);
} else {
@@ -490,7 +493,18 @@ public class Dispatcher {
return DiffView.UNIFIED_DIFF.equals(Gerrit.getUserPreferences().diffView());
}
private static void unified(final String token,
private static void unified(final String token, final PatchSet.Id baseId,
final Patch.Key id, final DisplaySide side, final int line) {
GWT.runAsync(new AsyncSplit(token) {
@Override
public void onSuccess() {
Gerrit.display(token,
new Unified(baseId, id.getParentKey(), id.get(), side, line));
}
});
}
private static void unified1(final String token,
final PatchSet.Id baseId,
final Patch.Key id) {
GWT.runAsync(new AsyncSplit(token) {

View File

@@ -15,83 +15,31 @@
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 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 SideBySide}. */
class 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 final native JavaScriptObject initOnClick(DisplaySide s) /*-{
return $entry(function(e){
@com.google.gerrit.client.diff.ChunkManager::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 static final native void onClick(Element e, JavaScriptObject f)
/** Colors modified regions for {@link SideBySide} and {@link Unified}. */
abstract class ChunkManager {
static final native void onClick(Element e, JavaScriptObject f)
/*-{ e.onclick = f }-*/;
private final SideBySide host;
private final CodeMirror cmA;
private final CodeMirror cmB;
private final Scrollbar scrollbar;
private final LineMapper mapper;
private List<DiffChunkInfo> chunks;
private List<TextMarker> markers;
private List<Runnable> undo;
private List<LineWidget> padding;
private List<Element> paddingDivs;
ChunkManager(SideBySide host,
CodeMirror cmA,
CodeMirror cmB,
Scrollbar scrollbar) {
this.host = host;
this.cmA = cmA;
this.cmB = cmB;
ChunkManager(Scrollbar scrollbar) {
this.scrollbar = scrollbar;
this.mapper = new LineMapper();
}
@@ -100,8 +48,14 @@ class ChunkManager {
return mapper;
}
DiffChunkInfo getFirst() {
return !chunks.isEmpty() ? chunks.get(0) : null;
Scrollbar getScrollbar() {
return scrollbar;
}
abstract DiffChunkInfo getFirst();
List<TextMarker> getMarkers() {
return markers;
}
void reset() {
@@ -112,133 +66,20 @@ class ChunkManager {
for (Runnable r : undo) {
r.run();
}
for (LineWidget w : padding) {
w.clear();
}
}
void render(DiffInfo diff) {
chunks = new ArrayList<>();
abstract void render(DiffInfo diff);
void render() {
markers = new ArrayList<>();
undo = new ArrayList<>();
padding = new ArrayList<>();
paddingDivs = new ArrayList<>();
String diffColor = diff.metaA() == null || diff.metaB() == 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);
}
}
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 = 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.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);
mapper.appendReplace(aLen, bLen);
int endA = mapper.getLineA() - 1;
int endB = mapper.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", DiffTable.style.intralineBg())
.set("readOnly", true);
Configuration diff = Configuration.create()
.set("className", DiffTable.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()) {
markers.add(cm.markText(last, from, bg));
} else {
markers.add(cm.markText(Pos.create(from.line(), 0), from, bg));
}
markers.add(cm.markText(from, to, diff));
last = to;
colorLines(cm, LineClassWhere.BACKGROUND,
DiffTable.style.diff(),
from.line(), to.line());
}
}
private void colorLines(CodeMirror cm, String color, int line, int cnt) {
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,
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++) {
@@ -255,78 +96,35 @@ class ChunkManager {
}
}
/**
* 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(DiffTable.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)));
abstract Runnable diffChunkNav(final CodeMirror cm, final Direction dir);
void diffChunkNavHelper(List<? extends DiffChunkInfo> chunks, CodeMirror cm, int res, Direction dir) {
if (res < 0) {
res = -res - (dir == Direction.PREV ? 1 : 2);
}
res = res + (dir == Direction.PREV ? -1 : 1);
if (res < 0 || chunks.size() <= res) {
return;
}
}
private void addDiffChunk(CodeMirror cmToPad, 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.extras().hasActiveLine()
? cm.getLineNumber(cm.extras().activeLine())
: 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() == 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(Pos.create(target.getStart(), 0));
targetCm.focus();
targetCm.scrollToY(
targetCm.heightAtLine(target.getStart(), "local") -
0.5 * cmB.scrollbarV().getClientHeight());
DiffChunkInfo lookUp = chunks.get(res);
// If edit, skip the deletion chunk and set focus on the insertion one.
if (lookUp.isEdit() && lookUp.getSide() == A) {
res = res + (dir == Direction.PREV ? -1 : 1);
if (res < 0 || chunks.size() <= res) {
return;
}
};
}
DiffChunkInfo target = chunks.get(res);
cm.setCursor(Pos.create(getCmLine(target.getStart(), target.getSide())));
cm.focus();
cm.scrollToY(
cm.heightAtLine(target.getStart(), "local") -
0.5 * cm.scrollbarV().getClientHeight());
}
private Comparator<DiffChunkInfo> getDiffChunkComparator() {
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
@@ -349,23 +147,5 @@ class ChunkManager {
};
}
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;
}
}
abstract int getCmLine(int line, DisplaySide side);
}

View File

@@ -61,7 +61,7 @@ abstract class CommentBox extends Composite {
fromTo.from(),
fromTo.to(),
Configuration.create()
.set("className", DiffTable.style.range()));
.set("className", Resources.I.diffTableStyle().range()));
}
addDomHandler(new MouseOverHandler() {
@Override
@@ -109,7 +109,7 @@ abstract class CommentBox extends Composite {
fromTo.from(),
fromTo.to(),
Configuration.create()
.set("className", DiffTable.style.rangeHighlight()));
.set("className", Resources.I.diffTableStyle().rangeHighlight()));
} else if (!highlight && rangeHighlightMarker != null) {
rangeHighlightMarker.clear();
rangeHighlightMarker = null;

View File

@@ -14,9 +14,6 @@
package com.google.gerrit.client.diff;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
@@ -30,39 +27,28 @@ import net.codemirror.lib.TextMarker.FromTo;
/**
* LineWidget attached to a CodeMirror container.
*
* When a comment is placed on a line a CommentWidget is created on both sides.
* The group tracks all comment boxes on that same line, and also includes an
* empty padding element to keep subsequent lines vertically aligned.
* When a comment is placed on a line a CommentWidget is created.
*/
class CommentGroup extends Composite {
static void pair(CommentGroup a, CommentGroup b) {
a.peer = b;
b.peer = a;
}
abstract class CommentGroup extends Composite {
private final CommentManager manager;
private final CodeMirror cm;
private final DisplaySide side;
private final int line;
private final FlowPanel comments;
private final Element padding;
private LineWidget lineWidget;
private Timer resizeTimer;
private CommentGroup peer;
CommentGroup(CommentManager manager, CodeMirror cm, int line) {
CommentGroup(CommentManager manager, CodeMirror cm, DisplaySide side, int line) {
this.manager = manager;
this.cm = cm;
this.side = side;
this.line = line;
comments = new FlowPanel();
comments.setStyleName(Resources.I.style().commentWidgets());
comments.setVisible(false);
initWidget(new SimplePanel(comments));
padding = DOM.createDiv();
padding.setClassName(DiffTable.style.padding());
ChunkManager.focusOnClick(padding, cm.side());
getElement().appendChild(padding);
}
CommentManager getCommentManager() {
@@ -73,10 +59,6 @@ class CommentGroup extends Composite {
return cm;
}
CommentGroup getPeer() {
return peer;
}
int getLine() {
return line;
}
@@ -138,33 +120,19 @@ class CommentGroup extends Composite {
void remove(DraftBox box) {
comments.remove(box);
comments.setVisible(0 < getBoxCount());
if (0 < getBoxCount() || 0 < peer.getBoxCount()) {
resize();
} else {
detach();
peer.detach();
}
}
private void detach() {
void detach() {
if (lineWidget != null) {
lineWidget.clear();
lineWidget = null;
updateSelection();
}
manager.clearLine(cm.side(), line, this);
manager.clearLine(side, line, this);
removeFromParent();
}
void attachPair(DiffTable parent) {
if (lineWidget == null && peer.lineWidget == null) {
this.attach(parent);
peer.attach(parent);
}
}
private void attach(DiffTable parent) {
void attach(DiffTable parent) {
parent.add(this);
lineWidget = cm.addLineWidget(Math.max(0, line - 1), getElement(),
Configuration.create()
@@ -174,33 +142,6 @@ class CommentGroup extends Composite {
.set("insertAt", 0));
}
void handleRedraw() {
lineWidget.onRedraw(new Runnable() {
@Override
public void run() {
if (canComputeHeight() && peer.canComputeHeight()) {
if (resizeTimer != null) {
resizeTimer.cancel();
resizeTimer = null;
}
adjustPadding(CommentGroup.this, peer);
} else if (resizeTimer == null) {
resizeTimer = new Timer() {
@Override
public void run() {
if (canComputeHeight() && peer.canComputeHeight()) {
cancel();
resizeTimer = null;
adjustPadding(CommentGroup.this, peer);
}
}
};
resizeTimer.scheduleRepeating(5);
}
}
});
}
@Override
protected void onUnload() {
super.onUnload();
@@ -209,13 +150,7 @@ class CommentGroup extends Composite {
}
}
void resize() {
if (lineWidget != null) {
adjustPadding(this, peer);
}
}
private void updateSelection() {
void updateSelection() {
if (cm.somethingSelected()) {
FromTo r = cm.getSelectedRange();
if (r.to().line() >= line) {
@@ -224,27 +159,37 @@ class CommentGroup extends Composite {
}
}
private boolean canComputeHeight() {
boolean canComputeHeight() {
return !comments.isVisible() || comments.getOffsetHeight() > 0;
}
private int computeHeight() {
if (comments.isVisible()) {
// Include margin-bottom: 5px from CSS class.
return comments.getOffsetHeight() + 5;
}
return 0;
LineWidget getLineWidget() {
return lineWidget;
}
private static void adjustPadding(CommentGroup a, CommentGroup b) {
int apx = a.computeHeight();
int bpx = b.computeHeight();
int h = Math.max(apx, bpx);
a.padding.getStyle().setHeight(Math.max(0, h - apx), Unit.PX);
b.padding.getStyle().setHeight(Math.max(0, h - bpx), Unit.PX);
a.lineWidget.changed();
b.lineWidget.changed();
a.updateSelection();
b.updateSelection();
void setLineWidget(LineWidget widget) {
lineWidget = widget;
}
Timer getResizeTimer() {
return resizeTimer;
}
void setResizeTimer(Timer timer) {
resizeTimer = timer;
}
FlowPanel getComments() {
return comments;
}
CommentManager getManager() {
return manager;
}
abstract void init(DiffTable parent);
abstract void handleRedraw();
abstract void resize();
}

View File

@@ -1,4 +1,4 @@
// Copyright (C) 2013 The Android Open Source Project
// Copyright (C) 2014 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.
@@ -14,7 +14,6 @@
package com.google.gerrit.client.diff;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.changes.CommentInfo;
import com.google.gerrit.client.patches.SkippedLine;
import com.google.gerrit.client.rpc.CallbackGroup;
@@ -25,41 +24,32 @@ import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.JsArray;
import net.codemirror.lib.CodeMirror;
import net.codemirror.lib.CodeMirror.LineHandle;
import net.codemirror.lib.Pos;
import net.codemirror.lib.TextMarker.FromTo;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
/** Tracks comment widgets for {@link SideBySide}. */
class CommentManager {
private final SideBySide host;
/** Tracks comment widgets for {@link DiffScreen}. */
abstract class CommentManager {
private final PatchSet.Id base;
private final PatchSet.Id revision;
private final String path;
private final CommentLinkProcessor commentLinkProcessor;
private final Map<String, PublishedBox> published;
private final SortedMap<Integer, CommentGroup> sideA;
private final SortedMap<Integer, CommentGroup> sideB;
private final Set<DraftBox> unsavedDrafts;
private boolean attached;
private boolean expandAll;
private boolean open;
CommentManager(SideBySide host,
PatchSet.Id base, PatchSet.Id revision,
CommentManager(
PatchSet.Id base,
PatchSet.Id revision,
String path,
CommentLinkProcessor clp,
boolean open) {
this.host = host;
this.base = base;
this.revision = revision;
this.path = path;
@@ -67,111 +57,42 @@ class CommentManager {
this.open = open;
published = new HashMap<>();
sideA = new TreeMap<>();
sideB = new TreeMap<>();
unsavedDrafts = new HashSet<>();
}
SideBySide getSideBySide() {
return host;
void setAttached(boolean attached) {
this.attached = attached;
}
void setExpandAllComments(boolean b) {
expandAll = b;
for (CommentGroup g : sideA.values()) {
g.setOpenAll(b);
}
for (CommentGroup g : sideB.values()) {
g.setOpenAll(b);
}
boolean isAttached() {
return attached;
}
Runnable commentNav(final CodeMirror src, final Direction dir) {
return new Runnable() {
@Override
public void run() {
// Every comment appears in both side maps as a linked pair.
// It is only necessary to search one side to find a comment
// on either side of the editor pair.
SortedMap<Integer, CommentGroup> map = map(src.side());
int line = src.extras().hasActiveLine()
? src.getLineNumber(src.extras().activeLine()) + 1
: 0;
if (dir == Direction.NEXT) {
map = map.tailMap(line + 1);
if (map.isEmpty()) {
return;
}
line = map.firstKey();
} else {
map = map.headMap(line);
if (map.isEmpty()) {
return;
}
line = map.lastKey();
}
CommentGroup g = map.get(line);
if (g.getBoxCount() == 0) {
g = g.getPeer();
}
CodeMirror cm = g.getCm();
double y = cm.heightAtLine(g.getLine() - 1, "local");
cm.setCursor(Pos.create(g.getLine() - 1));
cm.scrollToY(y - 0.5 * cm.scrollbarV().getClientHeight());
cm.focus();
}
};
void setExpandAll(boolean expandAll) {
this.expandAll = expandAll;
}
void render(CommentsCollections in, boolean expandAll) {
if (in.publishedBase != null) {
renderPublished(DisplaySide.A, in.publishedBase);
}
if (in.publishedRevision != null) {
renderPublished(DisplaySide.B, in.publishedRevision);
}
if (in.draftsBase != null) {
renderDrafts(DisplaySide.A, in.draftsBase);
}
if (in.draftsRevision != null) {
renderDrafts(DisplaySide.B, in.draftsRevision);
}
if (expandAll) {
setExpandAllComments(true);
}
for (CommentGroup g : sideA.values()) {
g.attachPair(host.diffTable);
}
for (CommentGroup g : sideB.values()) {
g.attachPair(host.diffTable);
g.handleRedraw();
}
attached = true;
boolean isExpandAll() {
return expandAll;
}
private void renderPublished(DisplaySide forSide, JsArray<CommentInfo> in) {
for (CommentInfo info : Natives.asList(in)) {
DisplaySide side = displaySide(info, forSide);
if (side != null) {
CommentGroup group = group(side, info.line());
PublishedBox box = new PublishedBox(
group,
commentLinkProcessor,
getPatchSetIdFromSide(side),
info,
open);
group.add(box);
box.setAnnotation(host.diffTable.scrollbar.comment(
host.getCmFromSide(side),
Math.max(0, info.line() - 1)));
published.put(info.id(), box);
}
}
boolean isOpen() {
return open;
}
private void renderDrafts(DisplaySide forSide, JsArray<CommentInfo> in) {
String getPath() {
return path;
}
Map<String, PublishedBox> getPublished() {
return published;
}
CommentLinkProcessor getCommentLinkProcessor() {
return commentLinkProcessor;
}
void renderDrafts(DisplaySide forSide, JsArray<CommentInfo> in) {
for (CommentInfo info : Natives.asList(in)) {
DisplaySide side = displaySide(info, forSide);
if (side != null) {
@@ -180,197 +101,6 @@ class CommentManager {
}
}
/**
* Create a new {@link DraftBox} at the specified line and focus it.
*
* @param side which side the draft will appear on.
* @param line the line the draft will be at. Lines are 1-based. Line 0 is a
* special case creating a file level comment.
*/
void insertNewDraft(DisplaySide side, int line) {
if (line == 0) {
host.getSkipManager().ensureFirstLineIsVisible();
}
CommentGroup group = group(side, line);
if (0 < group.getBoxCount()) {
CommentBox last = group.getCommentBox(group.getBoxCount() - 1);
if (last instanceof DraftBox) {
((DraftBox)last).setEdit(true);
} else {
((PublishedBox)last).doReply();
}
} else {
addDraftBox(side, CommentInfo.create(
path,
getStoredSideFromDisplaySide(side),
line,
null)).setEdit(true);
}
}
DraftBox addDraftBox(DisplaySide side, CommentInfo info) {
CommentGroup group = group(side, info.line());
DraftBox box = new DraftBox(
group,
commentLinkProcessor,
getPatchSetIdFromSide(side),
info,
expandAll);
if (info.inReplyTo() != null) {
PublishedBox r = published.get(info.inReplyTo());
if (r != null) {
r.setReplyBox(box);
}
}
group.add(box);
box.setAnnotation(host.diffTable.scrollbar.draft(
host.getCmFromSide(side),
Math.max(0, info.line() - 1)));
return box;
}
private DisplaySide displaySide(CommentInfo info, DisplaySide forSide) {
if (info.side() == Side.PARENT) {
return base == null ? DisplaySide.A : null;
}
return forSide;
}
List<SkippedLine> splitSkips(int context, List<SkippedLine> skips) {
if (sideB.containsKey(0)) {
// Special case of file comment; cannot skip first line.
for (SkippedLine skip : skips) {
if (skip.getStartB() == 0) {
skip.incrementStart(1);
}
}
}
// TODO: This is not optimal, but shouldn't be too costly in most cases.
// Maybe rewrite after done keeping track of diff chunk positions.
for (int boxLine : sideB.tailMap(1).keySet()) {
List<SkippedLine> temp = new ArrayList<>(skips.size() + 2);
for (SkippedLine skip : skips) {
int startLine = skip.getStartB();
int deltaBefore = boxLine - startLine;
int deltaAfter = startLine + skip.getSize() - boxLine;
if (deltaBefore < -context || deltaAfter < -context) {
temp.add(skip); // Size guaranteed to be greater than 1
} else if (deltaBefore > context && deltaAfter > context) {
SkippedLine before = new SkippedLine(
skip.getStartA(), skip.getStartB(),
skip.getSize() - deltaAfter - context);
skip.incrementStart(deltaBefore + context);
checkAndAddSkip(temp, before);
checkAndAddSkip(temp, skip);
} else if (deltaAfter > context) {
skip.incrementStart(deltaBefore + context);
checkAndAddSkip(temp, skip);
} else if (deltaBefore > context) {
skip.reduceSize(deltaAfter + context);
checkAndAddSkip(temp, skip);
}
}
if (temp.isEmpty()) {
return temp;
}
skips = temp;
}
return skips;
}
private static void checkAndAddSkip(List<SkippedLine> out, SkippedLine s) {
if (s.getSize() > 1) {
out.add(s);
}
}
void clearLine(DisplaySide side, int line, CommentGroup group) {
SortedMap<Integer, CommentGroup> map = map(side);
if (map.get(line) == group) {
map.remove(line);
}
}
Runnable toggleOpenBox(final CodeMirror cm) {
return new Runnable() {
@Override
public void run() {
if (cm.extras().hasActiveLine()) {
CommentGroup w = map(cm.side()).get(
cm.getLineNumber(cm.extras().activeLine()) + 1);
if (w != null) {
w.openCloseLast();
}
}
}
};
}
Runnable openCloseAll(final CodeMirror cm) {
return new Runnable() {
@Override
public void run() {
if (cm.extras().hasActiveLine()) {
CommentGroup w = map(cm.side()).get(
cm.getLineNumber(cm.extras().activeLine()) + 1);
if (w != null) {
w.openCloseAll();
}
}
}
};
}
Runnable insertNewDraft(final CodeMirror cm) {
if (!Gerrit.isSignedIn()) {
return new Runnable() {
@Override
public void run() {
String token = host.getToken();
if (cm.extras().hasActiveLine()) {
LineHandle handle = cm.extras().activeLine();
int line = cm.getLineNumber(handle) + 1;
token += "@" + (cm.side() == DisplaySide.A ? "a" : "") + line;
}
Gerrit.doSignIn(token);
}
};
}
return new Runnable() {
@Override
public void run() {
if (cm.extras().hasActiveLine()) {
newDraft(cm, cm.getLineNumber(cm.extras().activeLine()) + 1);
}
}
};
}
void newDraft(CodeMirror cm, int line) {
if (cm.somethingSelected()) {
FromTo fromTo = cm.getSelectedRange();
Pos end = fromTo.to();
if (end.ch() == 0) {
end.line(end.line() - 1);
end.ch(cm.getLine(end.line()).length());
}
addDraftBox(cm.side(), CommentInfo.create(
path,
getStoredSideFromDisplaySide(cm.side()),
line,
CommentRange.create(fromTo))).setEdit(true);
cm.setSelection(cm.getCursor());
} else {
insertNewDraft(cm.side(), line);
}
}
void setUnsaved(DraftBox box, boolean isUnsaved) {
if (isUnsaved) {
unsavedDrafts.add(box);
@@ -385,52 +115,42 @@ class CommentManager {
}
}
private CommentGroup group(DisplaySide side, int line) {
CommentGroup w = map(side).get(line);
if (w != null) {
return w;
}
int lineA;
int lineB;
if (line == 0) {
lineA = lineB = 0;
} else if (side == DisplaySide.A) {
lineA = line;
lineB = host.lineOnOther(side, line - 1).getLine() + 1;
} else {
lineA = host.lineOnOther(side, line - 1).getLine() + 1;
lineB = line;
}
CommentGroup a = newGroup(DisplaySide.A, lineA);
CommentGroup b = newGroup(DisplaySide.B, lineB);
CommentGroup.pair(a, b);
sideA.put(lineA, a);
sideB.put(lineB, b);
if (attached) {
a.attachPair(host.diffTable);
b.handleRedraw();
}
return side == DisplaySide.A ? a : b;
}
private CommentGroup newGroup(DisplaySide side, int line) {
return new CommentGroup(this, host.getCmFromSide(side), line);
}
private SortedMap<Integer, CommentGroup> map(DisplaySide side) {
return side == DisplaySide.A ? sideA : sideB;
}
private Side getStoredSideFromDisplaySide(DisplaySide side) {
Side getStoredSideFromDisplaySide(DisplaySide side) {
return side == DisplaySide.A && base == null ? Side.PARENT : Side.REVISION;
}
private PatchSet.Id getPatchSetIdFromSide(DisplaySide side) {
PatchSet.Id getPatchSetIdFromSide(DisplaySide side) {
return side == DisplaySide.A && base != null ? base : revision;
}
DisplaySide displaySide(CommentInfo info, DisplaySide forSide) {
if (info.side() == Side.PARENT) {
return base == null ? DisplaySide.A : null;
}
return forSide;
}
abstract void insertNewDraft(DisplaySide side, int line);
abstract Runnable newDraftCallback(final CodeMirror cm);
abstract DraftBox addDraftBox(DisplaySide side, CommentInfo info);
abstract void setExpandAllComments(boolean b);
abstract Runnable commentNav(CodeMirror src, Direction dir);
abstract void clearLine(DisplaySide side, int line, CommentGroup group);
abstract void renderPublished(DisplaySide forSide, JsArray<CommentInfo> in);
abstract List<SkippedLine> splitSkips(int context, List<SkippedLine> skips);
abstract void newDraftOnGutterClick(CodeMirror cm, String gutterClass, int line);
abstract Runnable toggleOpenBox(final CodeMirror cm);
abstract Runnable openCloseAll(final CodeMirror cm);
abstract DiffScreen getDiffScreen();
}

View File

@@ -118,6 +118,26 @@ public class DiffInfo extends JavaScriptObject {
return s.toString();
}
public final String textUnified() {
StringBuilder s = new StringBuilder();
JsArray<Region> c = content();
for (int i = 0; i < c.length(); i++) {
Region r = c.get(i);
if (r.ab() != null) {
append(s, r.ab());
} else {
if (r.a() != null) {
append(s, r.a());
}
if (r.b() != null) {
append(s, r.b());
}
}
// TODO skip may need to be handled
}
return s.toString();
}
private static void append(StringBuilder s, JsArrayString lines) {
for (int i = 0; i < lines.length(); i++) {
s.append(lines.get(i)).append('\n');

View File

@@ -0,0 +1,907 @@
// 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 impl ied.
// 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.extensions.client.DiffPreferencesInfo.WHOLE_FILE_CONTEXT;
import static java.lang.Double.POSITIVE_INFINITY;
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.JumpKeys;
import com.google.gerrit.client.account.DiffPreferences;
import com.google.gerrit.client.change.ChangeScreen;
import com.google.gerrit.client.change.FileTable;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.changes.ChangeList;
import com.google.gerrit.client.diff.DiffInfo.FileMeta;
import com.google.gerrit.client.diff.LineMapper.LineOnOtherInfo;
import com.google.gerrit.client.info.ChangeInfo;
import com.google.gerrit.client.info.ChangeInfo.EditInfo;
import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
import com.google.gerrit.client.info.FileInfo;
import com.google.gerrit.client.patches.PatchUtil;
import com.google.gerrit.client.projects.ConfigInfoCache;
import com.google.gerrit.client.rpc.CallbackGroup;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.Screen;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.RepeatingCommand;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwtexpui.globalkey.client.GlobalKey;
import com.google.gwtexpui.globalkey.client.KeyCommand;
import com.google.gwtexpui.globalkey.client.KeyCommandSet;
import com.google.gwtexpui.globalkey.client.ShowHelpCommand;
import net.codemirror.lib.CodeMirror;
import net.codemirror.lib.CodeMirror.BeforeSelectionChangeHandler;
import net.codemirror.lib.CodeMirror.GutterClickHandler;
import net.codemirror.lib.CodeMirror.LineHandle;
import net.codemirror.lib.KeyMap;
import net.codemirror.lib.Pos;
import net.codemirror.mode.ModeInfo;
import net.codemirror.mode.ModeInjector;
import net.codemirror.theme.ThemeLoader;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
/** Base class for SideBySide and Unified */
abstract class DiffScreen extends Screen {
static final KeyMap RENDER_ENTIRE_FILE_KEYMAP = KeyMap.create()
.propagate("Ctrl-F");
enum FileSize {
SMALL(0),
LARGE(500),
HUGE(4000);
final int lines;
FileSize(int n) {
this.lines = n;
}
}
enum DiffScreenType {
SIDE_BY_SIDE, UNIFIED
}
private final Change.Id changeId;
private final PatchSet.Id base;
private final PatchSet.Id revision;
private final String path;
private DisplaySide startSide;
private int startLine;
private DiffPreferences prefs;
private Change.Status changeStatus;
private HandlerRegistration resizeHandler;
private DiffInfo diff;
private FileSize fileSize;
private EditInfo edit;
private KeyCommandSet keysNavigation;
private KeyCommandSet keysAction;
private KeyCommandSet keysComment;
private List<HandlerRegistration> handlers;
private PreferencesAction prefsAction;
private int reloadVersionId;
@UiField(provided = true)
Header header;
DiffScreen(
PatchSet.Id base,
PatchSet.Id revision,
String path,
DisplaySide startSide,
int startLine,
DiffScreenType diffScreenType) {
this.base = base;
this.revision = revision;
this.changeId = revision.getParentKey();
this.path = path;
this.startSide = startSide;
this.startLine = startLine;
prefs = DiffPreferences.create(Gerrit.getDiffPreferences());
handlers = new ArrayList<>(6);
keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
header = new Header(keysNavigation, base, revision, path, diffScreenType);
}
@Override
protected void onInitUI() {
super.onInitUI();
setHeaderVisible(false);
setWindowTitle(FileInfo.getFileName(path));
}
@Override
protected void onLoad() {
super.onLoad();
CallbackGroup group1 = new CallbackGroup();
final CallbackGroup group2 = new CallbackGroup();
CodeMirror.initLibrary(group1.add(new AsyncCallback<Void>() {
final AsyncCallback<Void> themeCallback = group2.addEmpty();
@Override
public void onSuccess(Void result) {
// Load theme after CM library to ensure theme can override CSS.
ThemeLoader.loadTheme(prefs.theme(), themeCallback);
}
@Override
public void onFailure(Throwable caught) {
}
}));
DiffApi.diff(revision, path)
.base(base)
.wholeFile()
.intraline(prefs.intralineDifference())
.ignoreWhitespace(prefs.ignoreWhitespace())
.get(group1.addFinal(new GerritCallback<DiffInfo>() {
final AsyncCallback<Void> modeInjectorCb = group2.addEmpty();
@Override
public void onSuccess(DiffInfo diffInfo) {
diff = diffInfo;
fileSize = bucketFileSize(diffInfo);
if (prefs.syntaxHighlighting()) {
if (fileSize.compareTo(FileSize.SMALL) > 0) {
modeInjectorCb.onSuccess(null);
} else {
injectMode(diffInfo, modeInjectorCb);
}
} else {
modeInjectorCb.onSuccess(null);
}
}
}));
if (Gerrit.isSignedIn()) {
ChangeApi.edit(changeId.get(), group2.add(
new AsyncCallback<EditInfo>() {
@Override
public void onSuccess(EditInfo result) {
edit = result;
}
@Override
public void onFailure(Throwable caught) {
}
}));
}
final CommentsCollections comments = new CommentsCollections();
comments.load(base, revision, path, group2);
RestApi call = ChangeApi.detail(changeId.get());
ChangeList.addOptions(call, EnumSet.of(
ListChangesOption.ALL_REVISIONS));
call.get(group2.add(new AsyncCallback<ChangeInfo>() {
@Override
public void onSuccess(ChangeInfo info) {
changeStatus = info.status();
info.revisions().copyKeysIntoChildren("name");
if (edit != null) {
edit.setName(edit.commit().commit());
info.setEdit(edit);
info.revisions().put(edit.name(), RevisionInfo.fromEdit(edit));
}
String currentRevision = info.currentRevision();
boolean current = currentRevision != null &&
revision.get() == info.revision(currentRevision)._number();
JsArray<RevisionInfo> list = info.revisions().values();
RevisionInfo.sortRevisionInfoByNumber(list);
getDiffTable().set(prefs, list, diff, edit != null, current,
changeStatus.isOpen(), diff.binary());
header.setChangeInfo(info);
}
@Override
public void onFailure(Throwable caught) {
}
}));
ConfigInfoCache.get(changeId, group2.addFinal(
getScreenLoadCallback(comments)));
}
@Override
public void onShowView() {
super.onShowView();
Window.enableScrolling(false);
JumpKeys.enable(false);
if (getPrefs().hideTopMenu()) {
Gerrit.setHeaderVisible(false);
}
resizeHandler = Window.addResizeHandler(new ResizeHandler() {
@Override
public void onResize(ResizeEvent event) {
resizeCodeMirror();
}
});
}
KeyCommandSet getKeysNavigation() {
return keysNavigation;
}
KeyCommandSet getKeysAction() {
return keysAction;
}
KeyCommandSet getKeysComment() {
return keysComment;
}
@Override
protected void onUnload() {
super.onUnload();
removeKeyHandlerRegistrations();
if (getCommentManager() != null) {
CallbackGroup group = new CallbackGroup();
getCommentManager().saveAllDrafts(group);
group.done();
}
if (resizeHandler != null) {
resizeHandler.removeHandler();
resizeHandler = null;
}
for (CodeMirror cm : getCms()) {
if (cm != null) {
cm.getWrapperElement().removeFromParent();
}
}
if (prefsAction != null) {
prefsAction.hide();
}
Window.enableScrolling(true);
Gerrit.setHeaderVisible(true);
JumpKeys.enable(true);
}
private void removeKeyHandlerRegistrations() {
for (HandlerRegistration h : handlers) {
h.removeHandler();
}
handlers.clear();
}
void registerCmEvents(final CodeMirror cm) {
cm.on("cursorActivity", updateActiveLine(cm));
cm.on("focus", updateActiveLine(cm));
KeyMap keyMap = KeyMap.create()
.on("A", upToChange(true))
.on("U", upToChange(false))
.on("'['", header.navigate(Direction.PREV))
.on("']'", header.navigate(Direction.NEXT))
.on("R", header.toggleReviewed())
.on("O", getCommentManager().toggleOpenBox(cm))
.on("Enter", getCommentManager().toggleOpenBox(cm))
.on("N", maybeNextVimSearch(cm))
.on("E", openEditScreen(cm))
.on("P", getChunkManager().diffChunkNav(cm, Direction.PREV))
.on("Shift-M", header.reviewedAndNext())
.on("Shift-N", maybePrevVimSearch(cm))
.on("Shift-P", getCommentManager().commentNav(cm, Direction.PREV))
.on("Shift-O", getCommentManager().openCloseAll(cm))
.on("I", new Runnable() {
@Override
public void run() {
switch (getIntraLineStatus()) {
case OFF:
case OK:
toggleShowIntraline();
break;
case FAILURE:
case TIMEOUT:
default:
break;
}
}
})
.on("','", new Runnable() {
@Override
public void run() {
prefsAction.show();
}
})
.on("Shift-/", new Runnable() {
@Override
public void run() {
new ShowHelpCommand().onKeyPress(null);
}
})
.on("Space", new Runnable() {
@Override
public void run() {
cm.vim().handleKey("<C-d>");
}
})
.on("Shift-Space", new Runnable() {
@Override
public void run() {
cm.vim().handleKey("<C-u>");
}
})
.on("Ctrl-F", new Runnable() {
@Override
public void run() {
cm.vim().handleKey("/");
}
})
.on("Ctrl-A", new Runnable() {
@Override
public void run() {
cm.execCommand("selectAll");
}
});
if (revision.get() != 0) {
cm.on("beforeSelectionChange", onSelectionChange(cm));
cm.on("gutterClick", onGutterClick(cm));
keyMap.on("C", getCommentManager().newDraftCallback(cm));
}
cm.addKeyMap(keyMap);
}
void maybeRegisterRenderEntireFileKeyMap(CodeMirror cm) {
if (prefs.renderEntireFile()) {
cm.addKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
}
}
private BeforeSelectionChangeHandler onSelectionChange(final CodeMirror cm) {
return new BeforeSelectionChangeHandler() {
private InsertCommentBubble bubble;
@Override
public void handle(CodeMirror cm, Pos anchor, Pos head) {
if (anchor.equals(head)) {
if (bubble != null) {
bubble.setVisible(false);
}
return;
} else if (bubble == null) {
init(anchor);
} else {
bubble.setVisible(true);
}
bubble.position(cm.charCoords(head, "local"));
}
private void init(Pos anchor) {
bubble = new InsertCommentBubble(getCommentManager(), cm);
add(bubble);
cm.addWidget(anchor, bubble.getElement());
}
};
}
@Override
public void registerKeys() {
super.registerKeys();
keysNavigation.add(new UpToChangeCommand(revision, 0, 'u'));
keysNavigation.add(
new NoOpKeyCommand(0, 'j', PatchUtil.C.lineNext()),
new NoOpKeyCommand(0, 'k', PatchUtil.C.linePrev()));
keysNavigation.add(
new NoOpKeyCommand(0, 'n', PatchUtil.C.chunkNext2()),
new NoOpKeyCommand(0, 'p', PatchUtil.C.chunkPrev2()));
keysNavigation.add(
new NoOpKeyCommand(KeyCommand.M_SHIFT, 'n', PatchUtil.C.commentNext()),
new NoOpKeyCommand(KeyCommand.M_SHIFT, 'p', PatchUtil.C.commentPrev()));
keysNavigation.add(
new NoOpKeyCommand(KeyCommand.M_CTRL, 'f', Gerrit.C.keySearch()));
keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
keysAction.add(new NoOpKeyCommand(0, KeyCodes.KEY_ENTER,
PatchUtil.C.expandComment()));
keysAction.add(new NoOpKeyCommand(0, 'o', PatchUtil.C.expandComment()));
keysAction.add(new NoOpKeyCommand(
KeyCommand.M_SHIFT, 'o', PatchUtil.C.expandAllCommentsOnCurrentLine()));
if (Gerrit.isSignedIn()) {
keysAction.add(new KeyCommand(0, 'r', PatchUtil.C.toggleReviewed()) {
@Override
public void onKeyPress(KeyPressEvent event) {
header.toggleReviewed().run();
}
});
}
keysAction.add(new KeyCommand(
KeyCommand.M_SHIFT, 'm', PatchUtil.C.markAsReviewedAndGoToNext()) {
@Override
public void onKeyPress(KeyPressEvent event) {
header.reviewedAndNext().run();
}
});
keysAction.add(new KeyCommand(0, 'a', PatchUtil.C.openReply()) {
@Override
public void onKeyPress(KeyPressEvent event) {
upToChange(true).run();
}
});
keysAction.add(new KeyCommand(0, ',', PatchUtil.C.showPreferences()) {
@Override
public void onKeyPress(KeyPressEvent event) {
prefsAction.show();
}
});
if (getIntraLineStatus() == DiffInfo.IntraLineStatus.OFF
|| getIntraLineStatus() == DiffInfo.IntraLineStatus.OK) {
keysAction.add(new KeyCommand(0, 'i', PatchUtil.C.toggleIntraline()) {
@Override
public void onKeyPress(KeyPressEvent event) {
toggleShowIntraline();
}
});
}
if (Gerrit.isSignedIn()) {
keysAction.add(new NoOpKeyCommand(0, 'c', PatchUtil.C.commentInsert()));
keysComment = new KeyCommandSet(PatchUtil.C.commentEditorSet());
keysComment.add(new NoOpKeyCommand(KeyCommand.M_CTRL, 's',
PatchUtil.C.commentSaveDraft()));
keysComment.add(new NoOpKeyCommand(0, KeyCodes.KEY_ESCAPE,
PatchUtil.C.commentCancelEdit()));
} else {
keysComment = null;
}
}
void registerHandlers() {
removeKeyHandlerRegistrations();
handlers.add(GlobalKey.add(this, keysAction));
handlers.add(GlobalKey.add(this, keysNavigation));
if (keysComment != null) {
handlers.add(GlobalKey.add(this, keysComment));
}
handlers.add(ShowHelpCommand.addFocusHandler(getFocusHandler()));
}
void setupSyntaxHighlighting() {
if (prefs.syntaxHighlighting() && fileSize.compareTo(FileSize.SMALL) > 0) {
Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
@Override
public boolean execute() {
if (prefs.syntaxHighlighting() && isAttached()) {
setSyntaxHighlighting(prefs.syntaxHighlighting());
}
return false;
}
}, 250);
}
}
abstract CodeMirror newCm(
DiffInfo.FileMeta meta, String contents, Element parent);
void render(DiffInfo diff) {
header.setNoDiff(diff);
getChunkManager().render(diff);
}
abstract void setShowLineNumbers(boolean b);
void setShowIntraline(boolean b) {
if (b && getIntraLineStatus() == DiffInfo.IntraLineStatus.OFF) {
reloadDiffInfo();
} else if (b) {
getDiffTable().removeStyleName(Resources.I.diffTableStyle().noIntraline());
} else {
getDiffTable().addStyleName(Resources.I.diffTableStyle().noIntraline());
}
}
void toggleShowIntraline() {
prefs.intralineDifference(!prefs.intralineDifference());
setShowIntraline(prefs.intralineDifference());
prefsAction.update();
}
abstract void setSyntaxHighlighting(boolean b);
void setContext(final int context) {
operation(new Runnable() {
@Override
public void run() {
getSkipManager().removeAll();
getSkipManager().render(context, diff);
updateRenderEntireFile();
}
});
}
private int adjustCommitMessageLine(int line) {
/* When commit messages are shown in the diff screen they include
a header block that looks like this:
1 Parent: deadbeef (Parent commit title)
2 Author: A. U. Thor <author@example.com>
3 AuthorDate: 2015-02-27 19:20:52 +0900
4 Commit: A. U. Thor <author@example.com>
5 CommitDate: 2015-02-27 19:20:52 +0900
6 [blank line]
7 Commit message title
8
9 Commit message body
10 ...
11 ...
If the commit is a merge commit, both parent commits are listed in the
first two lines instead of a 'Parent' line:
1 Merge Of: deadbeef (Parent 1 commit title)
2 beefdead (Parent 2 commit title)
*/
// Offset to compensate for header lines until the blank line
// after 'CommitDate'
int offset = 6;
// Adjust for merge commits, which have two parent lines
if (diff.textB().startsWith("Merge")) {
offset += 1;
}
// If the cursor is inside the header line, reset to the first line of the
// commit message. Otherwise if the cursor is on an actual line of the commit
// message, adjust the line number to compensate for the header lines, so the
// focus is on the correct line.
if (line <= offset) {
return 1;
} else {
return line - offset;
}
}
private Runnable openEditScreen(final CodeMirror cm) {
return new Runnable() {
@Override
public void run() {
LineHandle handle = cm.extras().activeLine();
int line = cm.getLineNumber(handle) + 1;
if (Patch.COMMIT_MSG.equals(path)) {
line = adjustCommitMessageLine(line);
}
String token = Dispatcher.toEditScreen(revision, path, line);
if (!Gerrit.isSignedIn()) {
Gerrit.doSignIn(token);
} else {
Gerrit.display(token);
}
}
};
}
void updateRenderEntireFile() {
boolean entireFile = renderEntireFile();
for (CodeMirror cm : getCms()) {
cm.removeKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
if (entireFile) {
cm.addKeyMap(RENDER_ENTIRE_FILE_KEYMAP);
}
cm.setOption("viewportMargin", entireFile ? POSITIVE_INFINITY : 10);
}
}
void resizeCodeMirror() {
int height = getCodeMirrorHeight();
for (CodeMirror cm : getCms()) {
cm.adjustHeight(height);
}
}
abstract int getCodeMirrorHeight();
abstract ChunkManager getChunkManager();
abstract CommentManager getCommentManager();
abstract SkipManager getSkipManager();
DiffPreferences getPrefs() {
return prefs;
}
PatchSet.Id getRevision() {
return revision;
}
PatchSet.Id getBase() {
return base;
}
Change.Status getChangeStatus() {
return changeStatus;
}
int getStartLine() {
return startLine;
}
void setStartLine(int startLine) {
this.startLine = startLine;
}
DisplaySide getStartSide() {
return startSide;
}
void setStartSide(DisplaySide startSide) {
this.startSide = startSide;
}
DiffInfo getDiff() {
return diff;
}
FileSize getFileSize() {
return fileSize;
}
PreferencesAction getPrefsAction() {
return prefsAction;
}
void setPrefsAction(PreferencesAction prefsAction) {
this.prefsAction = prefsAction;
}
abstract void operation(final Runnable apply);
Runnable upToChange(final boolean openReplyBox) {
return new Runnable() {
@Override
public void run() {
CallbackGroup group = new CallbackGroup();
getCommentManager().saveAllDrafts(group);
group.done();
group.addListener(new GerritCallback<Void>() {
@Override
public void onSuccess(Void result) {
String b = base != null ? String.valueOf(base.get()) : null;
String rev = String.valueOf(revision.get());
Gerrit.display(
PageLinks.toChange(changeId, b, rev),
new ChangeScreen(changeId, b, rev, openReplyBox,
FileTable.Mode.REVIEW));
}
});
}
};
}
Runnable maybePrevVimSearch(final CodeMirror cm) {
return new Runnable() {
@Override
public void run() {
if (cm.vim().hasSearchHighlight()) {
cm.vim().handleKey("n");
} else {
getCommentManager().commentNav(cm, Direction.NEXT).run();
}
}
};
}
Runnable maybeNextVimSearch(final CodeMirror cm) {
return new Runnable() {
@Override
public void run() {
if (cm.vim().hasSearchHighlight()) {
cm.vim().handleKey("n");
} else {
getChunkManager().diffChunkNav(cm, Direction.NEXT).run();
}
}
};
}
boolean renderEntireFile() {
return prefs.renderEntireFile() && canRenderEntireFile(prefs);
}
boolean canRenderEntireFile(DiffPreferences prefs) {
// CodeMirror is too slow to layout an entire huge file.
return fileSize.compareTo(FileSize.HUGE) < 0
|| (prefs.context() != WHOLE_FILE_CONTEXT && prefs.context() < 100);
}
DiffInfo.IntraLineStatus getIntraLineStatus() {
return diff.intralineStatus();
}
void setThemeStyles(boolean d) {
if (d) {
getDiffTable().addStyleName(Resources.I.diffTableStyle().dark());
} else {
getDiffTable().removeStyleName(Resources.I.diffTableStyle().dark());
}
}
void setShowTabs(boolean show) {
for (CodeMirror cm : getCms()) {
cm.extras().showTabs(show);
}
}
void setLineLength(int columns) {
for (CodeMirror cm : getCms()) {
cm.extras().lineLength(columns);
}
}
String getContentType(DiffInfo.FileMeta meta) {
if (prefs.syntaxHighlighting() && meta != null
&& meta.contentType() != null) {
ModeInfo m = ModeInfo.findMode(meta.contentType(), path);
return m != null ? m.mime() : null;
}
return null;
}
String getContentType() {
return getContentType(diff.metaB());
}
void injectMode(DiffInfo diffInfo, AsyncCallback<Void> cb) {
new ModeInjector()
.add(getContentType(diffInfo.metaA()))
.add(getContentType(diffInfo.metaB()))
.inject(cb);
}
abstract void setAutoHideDiffHeader(boolean hide);
String getPath() {
return path;
}
void prefetchNextFile() {
String nextPath = header.getNextPath();
if (nextPath != null) {
DiffApi.diff(revision, nextPath)
.base(base)
.wholeFile()
.intraline(prefs.intralineDifference())
.ignoreWhitespace(prefs.ignoreWhitespace())
.get(new AsyncCallback<DiffInfo>() {
@Override
public void onSuccess(DiffInfo info) {
new ModeInjector()
.add(getContentType(info.metaA()))
.add(getContentType(info.metaB()))
.inject(CallbackGroup.<Void> emptyCallback());
}
@Override
public void onFailure(Throwable caught) {
}
});
}
}
void reloadDiffInfo() {
final int id = ++reloadVersionId;
DiffApi.diff(revision, path)
.base(base)
.wholeFile()
.intraline(prefs.intralineDifference())
.ignoreWhitespace(prefs.ignoreWhitespace())
.get(new GerritCallback<DiffInfo>() {
@Override
public void onSuccess(DiffInfo diffInfo) {
if (id == reloadVersionId && isAttached()) {
diff = diffInfo;
operation(new Runnable() {
@Override
public void run() {
getSkipManager().removeAll();
getChunkManager().reset();
getDiffTable().scrollbar.removeDiffAnnotations();
setShowIntraline(prefs.intralineDifference());
render(diff);
getSkipManager().render(prefs.context(), diff);
}
});
}
}
});
}
static FileSize bucketFileSize(DiffInfo diff) {
FileMeta a = diff.metaA();
FileMeta b = diff.metaB();
FileSize[] sizes = FileSize.values();
for (int i = sizes.length - 1; 0 <= i; i--) {
FileSize s = sizes[i];
if ((a != null && s.lines <= a.lines())
|| (b != null && s.lines <= b.lines())) {
return s;
}
}
return FileSize.SMALL;
}
abstract Runnable updateActiveLine(CodeMirror cm);
private GutterClickHandler onGutterClick(final CodeMirror cm) {
return new GutterClickHandler() {
@Override
public void handle(CodeMirror instance, final int line, final String gutterClass,
NativeEvent clickEvent) {
if (clickEvent.getButton() == NativeEvent.BUTTON_LEFT
&& !clickEvent.getMetaKey()
&& !clickEvent.getAltKey()
&& !clickEvent.getCtrlKey()
&& !clickEvent.getShiftKey()) {
cm.setCursor(Pos.create(line));
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
getCommentManager().newDraftOnGutterClick(cm, gutterClass, line + 1);
}
});
}
}
};
}
abstract FocusHandler getFocusHandler();
abstract CodeMirror[] getCms();
abstract CodeMirror getCmFromSide(DisplaySide side);
abstract DiffTable getDiffTable();
LineOnOtherInfo lineOnOther(DisplaySide side, int line) {
return getChunkManager().getLineMapper().lineOnOther(side, line);
}
abstract ScreenLoadCallback<ConfigInfoCache.Entry> getScreenLoadCallback(
CommentsCollections comments);
}

View File

@@ -0,0 +1,40 @@
/* 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.
*/
.range {
background-color: #ffd500 !important;
}
.rangeHighlight {
background-color: #ffff00 !important;
}
.fullscreen {
background-color: #f7f7f7;
border-bottom: 1px solid #ddd;
}
@external .diffHeader;
.diffHeader {
font-size: 12px;
font-weight: bold;
color: #5252ad;
}
.diffHeader pre {
margin: 0 0 3px 0;
}
@external .dark, .noIntraline;
.dark {}
.noIntraline {}

View File

@@ -18,54 +18,42 @@ import com.google.gerrit.client.account.DiffPreferences;
import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
import com.google.gerrit.reviewdb.client.Patch.ChangeType;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.GWT;
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.resources.client.CssResource;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwt.user.client.ui.Widget;
import net.codemirror.lib.CodeMirror;
/**
* A table with one row and two columns to hold the two CodeMirrors displaying
* the files to be diffed.
* Base class for SideBySideTable2 and UnifiedTable2
*/
class DiffTable extends Composite {
interface Binder extends UiBinder<HTMLPanel, DiffTable> {}
private static final Binder uiBinder = GWT.create(Binder.class);
abstract class DiffTable extends Composite {
static {
Resources.I.diffTableStyle().ensureInjected();
}
interface DiffTableStyle extends CssResource {
interface Style extends CssResource {
String fullscreen();
String intralineBg();
String dark();
String diff();
String noIntraline();
String range();
String rangeHighlight();
String showLineNumbers();
String hideA();
String hideB();
String padding();
String diffHeader();
}
@UiField Element cmA;
@UiField Element cmB;
Scrollbar scrollbar;
@UiField Element patchSetNavRow;
@UiField Element patchSetNavCellA;
@UiField Element patchSetNavCellB;
@UiField Element diffHeaderRow;
@UiField Element diffHeaderText;
@UiField FlowPanel widgets;
@UiField static DiffTableStyle style;
@UiField(provided = true)
PatchSetSelectBox patchSetSelectBoxA;
@@ -73,65 +61,31 @@ class DiffTable extends Composite {
@UiField(provided = true)
PatchSetSelectBox patchSetSelectBoxB;
private SideBySide parent;
private boolean header;
private boolean visibleA;
private ChangeType changeType;
Scrollbar scrollbar;
DiffTable(SideBySide parent, PatchSet.Id base, PatchSet.Id revision,
String path) {
DiffTable(DiffScreen parent, PatchSet.Id base, PatchSet.Id revision, String path) {
patchSetSelectBoxA = new PatchSetSelectBox(
parent, DisplaySide.A, revision.getParentKey(), base, path);
patchSetSelectBoxB = new PatchSetSelectBox(
parent, DisplaySide.B, revision.getParentKey(), revision, path);
PatchSetSelectBox.link(patchSetSelectBoxA, patchSetSelectBoxB);
initWidget(uiBinder.createAndBindUi(this));
this.scrollbar = new Scrollbar(this);
this.parent = parent;
this.visibleA = true;
}
boolean isVisibleA() {
return visibleA;
}
void setVisibleA(boolean show) {
visibleA = show;
if (show) {
removeStyleName(style.hideA());
parent.syncScroll(DisplaySide.B); // match B's viewport
} else {
addStyleName(style.hideA());
}
}
Runnable toggleA() {
return new Runnable() {
@Override
public void run() {
setVisibleA(!isVisibleA());
}
};
}
void setVisibleB(boolean show) {
if (show) {
removeStyleName(style.hideB());
parent.syncScroll(DisplaySide.A); // match A's viewport
} else {
addStyleName(style.hideB());
}
}
abstract boolean isVisibleA();
void setHeaderVisible(boolean show) {
DiffScreen parent = getDiffScreen();
if (show != UIObject.isVisible(patchSetNavRow)) {
UIObject.setVisible(patchSetNavRow, show);
UIObject.setVisible(diffHeaderRow, show && header);
if (show) {
parent.header.removeStyleName(style.fullscreen());
parent.header.removeStyleName(Resources.I.diffTableStyle().fullscreen());
} else {
parent.header.addStyleName(style.fullscreen());
parent.header.addStyleName(Resources.I.diffTableStyle().fullscreen());
}
parent.resizeCodeMirror();
}
@@ -182,17 +136,11 @@ class DiffTable extends Composite {
setHideEmptyPane(prefs.hideEmptyPane());
}
void setHideEmptyPane(boolean hide) {
if (changeType == ChangeType.ADDED) {
setVisibleA(!hide);
} else if (changeType == ChangeType.DELETED) {
setVisibleB(!hide);
}
}
abstract void setHideEmptyPane(boolean hide);
void refresh() {
if (header) {
CodeMirror cm = parent.getCmFromSide(DisplaySide.A);
CodeMirror cm = getDiffScreen().getCmFromSide(DisplaySide.A);
diffHeaderText.getStyle().setMarginLeft(
cm.getGutterElement().getOffsetWidth(),
Unit.PX);
@@ -202,4 +150,6 @@ class DiffTable extends Composite {
void add(Widget widget) {
widgets.add(widget);
}
abstract DiffScreen getDiffScreen();
}

View File

@@ -20,6 +20,7 @@ import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.changes.ReviewInfo;
import com.google.gerrit.client.changes.Util;
import com.google.gerrit.client.diff.DiffInfo.Region;
import com.google.gerrit.client.diff.DiffScreen.DiffScreenType;
import com.google.gerrit.client.info.ChangeInfo;
import com.google.gerrit.client.info.ChangeInfo.RevisionInfo;
import com.google.gerrit.client.info.FileInfo;
@@ -89,6 +90,7 @@ public class Header extends Composite {
private final PatchSet.Id base;
private final PatchSet.Id patchSetId;
private final String path;
private final DiffScreenType diffScreenType;
private boolean hasPrev;
private boolean hasNext;
private String nextPath;
@@ -96,12 +98,13 @@ public class Header extends Composite {
private ReviewedState reviewedState;
Header(KeyCommandSet keys, PatchSet.Id base, PatchSet.Id patchSetId,
String path) {
String path, DiffScreenType diffSreenType) {
initWidget(uiBinder.createAndBindUi(this));
this.keys = keys;
this.base = base;
this.patchSetId = patchSetId;
this.path = path;
this.diffScreenType = diffSreenType;
if (!Gerrit.isSignedIn()) {
reviewed.getElement().getStyle().setVisibility(Visibility.HIDDEN);
@@ -267,9 +270,9 @@ public class Header extends Composite {
}
private String url(FileInfo info) {
return info.binary()
? Dispatcher.toUnified(base, patchSetId, info.path())
: Dispatcher.toSideBySide(base, patchSetId, info.path());
return diffScreenType == DiffScreenType.UNIFIED || info.binary()
? Dispatcher.toUnified(base, patchSetId, info.path())
: Dispatcher.toSideBySide(base, patchSetId, info.path());
}
private KeyCommand setupNav(InlineHyperlink link, char key, String help, FileInfo info) {

View File

@@ -43,7 +43,7 @@ class InsertCommentBubble extends Composite {
@Override
public void onClick(ClickEvent event) {
setVisible(false);
commentManager.insertNewDraft(cm).run();
commentManager.newDraftCallback(cm).run();
}
}, ClickEvent.getType());
}

View File

@@ -54,7 +54,7 @@ class PatchSetSelectBox extends Composite {
@UiField HTMLPanel linkPanel;
@UiField BoxStyle style;
private SideBySide parent;
private DiffScreen parent;
private DisplaySide side;
private boolean sideA;
private String path;
@@ -63,7 +63,7 @@ class PatchSetSelectBox extends Composite {
private PatchSet.Id idActive;
private PatchSetSelectBox other;
PatchSetSelectBox(SideBySide parent,
PatchSetSelectBox(DiffScreen parent,
DisplaySide side,
Change.Id changeId,
PatchSet.Id revision,
@@ -143,10 +143,12 @@ class PatchSetSelectBox extends Composite {
if (sideA) {
assert other.idActive != null;
}
return new InlineHyperlink(label, Dispatcher.toSideBySide(
sideA ? id : other.idActive,
sideA ? other.idActive : id,
path));
PatchSet.Id diffBase = sideA ? id : other.idActive;
PatchSet.Id revision = sideA ? other.idActive : id;
return new InlineHyperlink(label, parent instanceof SideBySide
? Dispatcher.toSideBySide(diffBase, revision, path)
: Dispatcher.toUnified(diffBase, revision, path));
}
private Anchor createDownloadLink() {

View File

@@ -22,13 +22,13 @@ import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
import com.google.gwt.user.client.ui.Widget;
class PreferencesAction {
private final SideBySide view;
private final DiffScreen view;
private final DiffPreferences prefs;
private PopupPanel popup;
private PreferencesBox current;
private Widget partner;
PreferencesAction(SideBySide view, DiffPreferences prefs) {
PreferencesAction(DiffScreen view, DiffPreferences prefs) {
this.view = view;
this.prefs = prefs;
}

View File

@@ -58,6 +58,7 @@ import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.ToggleButton;
import com.google.gwt.user.client.ui.UIObject;
import net.codemirror.lib.CodeMirror;
import net.codemirror.mode.ModeInfo;
import net.codemirror.mode.ModeInjector;
import net.codemirror.theme.ThemeLoader;
@@ -73,7 +74,7 @@ public class PreferencesBox extends Composite {
String dialog();
}
private final SideBySide view;
private final DiffScreen view;
private DiffPreferences prefs;
private int contextLastValue;
private Timer updateContextTimer;
@@ -107,7 +108,7 @@ public class PreferencesBox extends Composite {
@UiField Button apply;
@UiField Button save;
public PreferencesBox(SideBySide view) {
public PreferencesBox(DiffScreen view) {
this.view = view;
initWidget(uiBinder.createAndBindUi(this));
@@ -181,9 +182,9 @@ public class PreferencesBox extends Composite {
lineNumbers.setValue(prefs.showLineNumbers());
emptyPane.setValue(!prefs.hideEmptyPane());
if (view != null) {
leftSide.setValue(view.diffTable.isVisibleA());
leftSide.setValue(view.getDiffTable().isVisibleA());
leftSide.setEnabled(!(prefs.hideEmptyPane()
&& view.diffTable.getChangeType() == ChangeType.ADDED));
&& view.getDiffTable().getChangeType() == ChangeType.ADDED));
} else {
UIObject.setVisible(leftSideLabel, false);
leftSide.setVisible(false);
@@ -315,8 +316,9 @@ public class PreferencesBox extends Composite {
@Override
public void run() {
int v = prefs.tabSize();
view.getCmFromSide(DisplaySide.A).setOption("tabSize", v);
view.getCmFromSide(DisplaySide.B).setOption("tabSize", v);
for (CodeMirror cm : view.getCms()) {
cm.setOption("tabSize", v);
}
}
});
}
@@ -378,21 +380,23 @@ public class PreferencesBox extends Composite {
@UiHandler("leftSide")
void onLeftSide(ValueChangeEvent<Boolean> e) {
view.diffTable.setVisibleA(e.getValue());
if (view.getDiffTable() instanceof SideBySideTable) {
((SideBySideTable) view.getDiffTable()).setVisibleA(e.getValue());
}
}
@UiHandler("emptyPane")
void onHideEmptyPane(ValueChangeEvent<Boolean> e) {
prefs.hideEmptyPane(!e.getValue());
if (view != null) {
view.diffTable.setHideEmptyPane(prefs.hideEmptyPane());
view.getDiffTable().setHideEmptyPane(prefs.hideEmptyPane());
if (prefs.hideEmptyPane()) {
if (view.diffTable.getChangeType() == ChangeType.ADDED) {
if (view.getDiffTable().getChangeType() == ChangeType.ADDED) {
leftSide.setValue(false);
leftSide.setEnabled(false);
}
} else {
leftSide.setValue(view.diffTable.isVisibleA());
leftSide.setValue(view.getDiffTable().isVisibleA());
leftSide.setEnabled(true);
}
}
@@ -468,8 +472,9 @@ public class PreferencesBox extends Composite {
@Override
public void run() {
boolean s = prefs.showWhitespaceErrors();
view.getCmFromSide(DisplaySide.A).setOption("showTrailingSpace", s);
view.getCmFromSide(DisplaySide.B).setOption("showTrailingSpace", s);
for (CodeMirror cm : view.getCms()) {
cm.setOption("showTrailingSpace", s);
}
}
});
}

View File

@@ -158,7 +158,7 @@ class PublishedBox extends CommentBox {
void doReply() {
if (!Gerrit.isSignedIn()) {
Gerrit.doSignIn(getCommentManager().getSideBySide().getToken());
Gerrit.doSignIn(getCommentManager().getDiffScreen().getToken());
} else if (replyBox == null) {
addReplyBox(false);
} else {
@@ -176,7 +176,7 @@ class PublishedBox extends CommentBox {
void onQuote(ClickEvent e) {
e.stopPropagation();
if (!Gerrit.isSignedIn()) {
Gerrit.doSignIn(getCommentManager().getSideBySide().getToken());
Gerrit.doSignIn(getCommentManager().getDiffScreen().getToken());
}
addReplyBox(true);
}
@@ -185,7 +185,7 @@ class PublishedBox extends CommentBox {
void onReplyDone(ClickEvent e) {
e.stopPropagation();
if (!Gerrit.isSignedIn()) {
Gerrit.doSignIn(getCommentManager().getSideBySide().getToken());
Gerrit.doSignIn(getCommentManager().getDiffScreen().getToken());
} else if (replyBox == null) {
done.setEnabled(false);
CommentInfo input = CommentInfo.createReply(comment);

View File

@@ -24,6 +24,7 @@ interface Resources extends ClientBundle {
@Source("CommentBox.css") CommentBox.Style style();
@Source("Scrollbar.css") Scrollbar.Style scrollbarStyle();
@Source("DiffTable.css") DiffTable.Style diffTableStyle();
/**
* tango icon library (public domain):

View File

@@ -20,7 +20,7 @@ import net.codemirror.lib.CodeMirror;
import net.codemirror.lib.ScrollInfo;
class ScrollSynchronizer {
private DiffTable diffTable;
private SideBySideTable diffTable;
private LineMapper mapper;
private ScrollCallback active;
private ScrollCallback callbackA;
@@ -28,7 +28,7 @@ class ScrollSynchronizer {
private CodeMirror cmB;
private boolean autoHideDiffTableHeader;
ScrollSynchronizer(DiffTable diffTable,
ScrollSynchronizer(SideBySideTable diffTable,
CodeMirror cmA, CodeMirror cmB,
LineMapper mapper) {
this.diffTable = diffTable;

View File

@@ -25,6 +25,6 @@ limitations under the License.
</ui:style>
<g:FlowPanel styleName='{style.sbs}'>
<d:Header ui:field='header'/>
<d:DiffTable ui:field='diffTable'/>
<d:SideBySideTable ui:field='diffTable'/>
</g:FlowPanel>
</ui:UiBinder>

View File

@@ -0,0 +1,280 @@
// 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 final 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();
LineMapper mapper = getLineMapper();
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) {
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);
}
}
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) {
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;
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);
mapper.appendReplace(aLen, bLen);
int endA = mapper.getLineA() - 1;
int endB = mapper.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) {
Scrollbar scrollbar = getScrollbar();
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, cm, res, dir);
}
};
}
@Override
int getCmLine(int line, DisplaySide side) {
return line;
}
}

View File

@@ -0,0 +1,128 @@
// 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.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Timer;
import net.codemirror.lib.CodeMirror;
/**
* LineWidget attached to a CodeMirror container.
*
* When a comment is placed on a line a CommentWidget is created on both sides.
* The group tracks all comment boxes on that same line, and also includes an
* empty padding element to keep subsequent lines vertically aligned.
*/
class SideBySideCommentGroup extends CommentGroup {
static void pair(SideBySideCommentGroup a, SideBySideCommentGroup b) {
a.peer = b;
b.peer = a;
}
private final Element padding;
private SideBySideCommentGroup peer;
SideBySideCommentGroup(SideBySideCommentManager manager, CodeMirror cm, DisplaySide side,
int line) {
super(manager, cm, side, line);
padding = DOM.createDiv();
padding.setClassName(SideBySideTable.style.padding());
SideBySideChunkManager.focusOnClick(padding, cm.side());
getElement().appendChild(padding);
}
SideBySideCommentGroup getPeer() {
return peer;
}
@Override
void remove(DraftBox box) {
super.remove(box);
if (0 < getBoxCount() || 0 < peer.getBoxCount()) {
resize();
} else {
detach();
peer.detach();
}
}
@Override
void init(DiffTable parent) {
if (getLineWidget() == null && peer.getLineWidget() == null) {
this.attach(parent);
peer.attach(parent);
}
}
@Override
void handleRedraw() {
getLineWidget().onRedraw(new Runnable() {
@Override
public void run() {
if (canComputeHeight() && peer.canComputeHeight()) {
if (getResizeTimer() != null) {
getResizeTimer().cancel();
setResizeTimer(null);
}
adjustPadding(SideBySideCommentGroup.this, peer);
} else if (getResizeTimer() == null) {
setResizeTimer(new Timer() {
@Override
public void run() {
if (canComputeHeight() && peer.canComputeHeight()) {
cancel();
setResizeTimer(null);
adjustPadding(SideBySideCommentGroup.this, peer);
}
}
});
getResizeTimer().scheduleRepeating(5);
}
}
});
}
@Override
void resize() {
if (getLineWidget() != null) {
adjustPadding(this, peer);
}
}
private int computeHeight() {
if (getComments().isVisible()) {
// Include margin-bottom: 5px from CSS class.
return getComments().getOffsetHeight() + 5;
}
return 0;
}
private static void adjustPadding(SideBySideCommentGroup a, SideBySideCommentGroup b) {
int apx = a.computeHeight();
int bpx = b.computeHeight();
int h = Math.max(apx, bpx);
a.padding.getStyle().setHeight(Math.max(0, h - apx), Unit.PX);
b.padding.getStyle().setHeight(Math.max(0, h - bpx), Unit.PX);
a.getLineWidget().changed();
b.getLineWidget().changed();
a.updateSelection();
b.updateSelection();
}
}

View File

@@ -0,0 +1,408 @@
// 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.Gerrit;
import com.google.gerrit.client.changes.CommentInfo;
import com.google.gerrit.client.patches.SkippedLine;
import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.ui.CommentLinkProcessor;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.JsArray;
import net.codemirror.lib.CodeMirror;
import net.codemirror.lib.CodeMirror.LineHandle;
import net.codemirror.lib.Pos;
import net.codemirror.lib.TextMarker.FromTo;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
/** Tracks comment widgets for {@link SideBySide}. */
class SideBySideCommentManager extends CommentManager {
private final SideBySide host;
private final SortedMap<Integer, SideBySideCommentGroup> sideA;
private final SortedMap<Integer, SideBySideCommentGroup> sideB;
SideBySideCommentManager(SideBySide host,
PatchSet.Id base, PatchSet.Id revision,
String path,
CommentLinkProcessor clp,
boolean open) {
super(base, revision, path, clp, open);
this.host = host;
sideA = new TreeMap<>();
sideB = new TreeMap<>();
}
@Override
SideBySide getDiffScreen() {
return host;
}
@Override
void setExpandAllComments(boolean b) {
setExpandAll(b);
for (SideBySideCommentGroup g : sideA.values()) {
g.setOpenAll(b);
}
for (SideBySideCommentGroup g : sideB.values()) {
g.setOpenAll(b);
}
}
@Override
Runnable commentNav(final CodeMirror src, final Direction dir) {
return new Runnable() {
@Override
public void run() {
// Every comment appears in both side maps as a linked pair.
// It is only necessary to search one side to find a comment
// on either side of the editor pair.
SortedMap<Integer, SideBySideCommentGroup> map = map(src.side());
int line = src.extras().hasActiveLine()
? src.getLineNumber(src.extras().activeLine()) + 1
: 0;
if (dir == Direction.NEXT) {
map = map.tailMap(line + 1);
if (map.isEmpty()) {
return;
}
line = map.firstKey();
} else {
map = map.headMap(line);
if (map.isEmpty()) {
return;
}
line = map.lastKey();
}
SideBySideCommentGroup g = map.get(line);
if (g.getBoxCount() == 0) {
g = g.getPeer();
}
CodeMirror cm = g.getCm();
double y = cm.heightAtLine(g.getLine() - 1, "local");
cm.setCursor(Pos.create(g.getLine() - 1));
cm.scrollToY(y - 0.5 * cm.scrollbarV().getClientHeight());
cm.focus();
}
};
}
void render(CommentsCollections in, boolean expandAll) {
if (in.publishedBase != null) {
renderPublished(DisplaySide.A, in.publishedBase);
}
if (in.publishedRevision != null) {
renderPublished(DisplaySide.B, in.publishedRevision);
}
if (in.draftsBase != null) {
renderDrafts(DisplaySide.A, in.draftsBase);
}
if (in.draftsRevision != null) {
renderDrafts(DisplaySide.B, in.draftsRevision);
}
if (expandAll) {
setExpandAllComments(true);
}
for (SideBySideCommentGroup g : sideA.values()) {
g.init(host.getDiffTable());
}
for (SideBySideCommentGroup g : sideB.values()) {
g.init(host.getDiffTable());
g.handleRedraw();
}
setAttached(true);
}
@Override
void renderPublished(DisplaySide forSide, JsArray<CommentInfo> in) {
for (CommentInfo info : Natives.asList(in)) {
DisplaySide side = displaySide(info, forSide);
if (side != null) {
SideBySideCommentGroup group = group(side, info.line());
PublishedBox box = new PublishedBox(
group,
getCommentLinkProcessor(),
getPatchSetIdFromSide(side),
info,
isOpen());
group.add(box);
box.setAnnotation(host.getDiffTable().scrollbar.comment(
host.getCmFromSide(side),
Math.max(0, info.line() - 1)));
getPublished().put(info.id(), box);
}
}
}
@Override
void newDraftOnGutterClick(CodeMirror cm, String gutterClass, int line) {
if (cm.somethingSelected()) {
FromTo fromTo = cm.getSelectedRange();
Pos end = fromTo.to();
if (end.ch() == 0) {
end.line(end.line() - 1);
end.ch(cm.getLine(end.line()).length());
}
addDraftBox(cm.side(), CommentInfo.create(
getPath(),
getStoredSideFromDisplaySide(cm.side()),
line,
CommentRange.create(fromTo))).setEdit(true);
cm.setSelection(cm.getCursor());
} else {
insertNewDraft(cm.side(), line);
}
}
/**
* Create a new {@link DraftBox} at the specified line and focus it.
*
* @param side which side the draft will appear on.
* @param line the line the draft will be at. Lines are 1-based. Line 0 is a
* special case creating a file level comment.
*/
@Override
void insertNewDraft(DisplaySide side, int line) {
if (line == 0) {
host.getSkipManager().ensureFirstLineIsVisible();
}
SideBySideCommentGroup group = group(side, line);
if (0 < group.getBoxCount()) {
CommentBox last = group.getCommentBox(group.getBoxCount() - 1);
if (last instanceof DraftBox) {
((DraftBox)last).setEdit(true);
} else {
((PublishedBox)last).doReply();
}
} else {
addDraftBox(side, CommentInfo.create(
getPath(),
getStoredSideFromDisplaySide(side),
line,
null)).setEdit(true);
}
}
@Override
DraftBox addDraftBox(DisplaySide side, CommentInfo info) {
SideBySideCommentGroup group = group(side, info.line());
DraftBox box = new DraftBox(
group,
getCommentLinkProcessor(),
getPatchSetIdFromSide(side),
info,
isExpandAll());
if (info.inReplyTo() != null) {
PublishedBox r = getPublished().get(info.inReplyTo());
if (r != null) {
r.setReplyBox(box);
}
}
group.add(box);
box.setAnnotation(host.getDiffTable().scrollbar.draft(
host.getCmFromSide(side),
Math.max(0, info.line() - 1)));
return box;
}
@Override
List<SkippedLine> splitSkips(int context, List<SkippedLine> skips) {
if (sideB.containsKey(0)) {
// Special case of file comment; cannot skip first line.
for (SkippedLine skip : skips) {
if (skip.getStartB() == 0) {
skip.incrementStart(1);
}
}
}
// TODO: This is not optimal, but shouldn't be too costly in most cases.
// Maybe rewrite after done keeping track of diff chunk positions.
for (int boxLine : sideB.tailMap(1).keySet()) {
List<SkippedLine> temp = new ArrayList<>(skips.size() + 2);
for (SkippedLine skip : skips) {
int startLine = skip.getStartB();
int deltaBefore = boxLine - startLine;
int deltaAfter = startLine + skip.getSize() - boxLine;
if (deltaBefore < -context || deltaAfter < -context) {
temp.add(skip); // Size guaranteed to be greater than 1
} else if (deltaBefore > context && deltaAfter > context) {
SkippedLine before = new SkippedLine(
skip.getStartA(), skip.getStartB(),
skip.getSize() - deltaAfter - context);
skip.incrementStart(deltaBefore + context);
checkAndAddSkip(temp, before);
checkAndAddSkip(temp, skip);
} else if (deltaAfter > context) {
skip.incrementStart(deltaBefore + context);
checkAndAddSkip(temp, skip);
} else if (deltaBefore > context) {
skip.reduceSize(deltaAfter + context);
checkAndAddSkip(temp, skip);
}
}
if (temp.isEmpty()) {
return temp;
}
skips = temp;
}
return skips;
}
private static void checkAndAddSkip(List<SkippedLine> out, SkippedLine s) {
if (s.getSize() > 1) {
out.add(s);
}
}
@Override
void clearLine(DisplaySide side, int line, CommentGroup group) {
SortedMap<Integer, SideBySideCommentGroup> map = map(side);
if (map.get(line) == group) {
map.remove(line);
}
}
@Override
Runnable toggleOpenBox(final CodeMirror cm) {
return new Runnable() {
@Override
public void run() {
if (cm.extras().hasActiveLine()) {
SideBySideCommentGroup w = map(cm.side()).get(
cm.getLineNumber(cm.extras().activeLine()) + 1);
if (w != null) {
w.openCloseLast();
}
}
}
};
}
@Override
Runnable openCloseAll(final CodeMirror cm) {
return new Runnable() {
@Override
public void run() {
if (cm.extras().hasActiveLine()) {
SideBySideCommentGroup w = map(cm.side()).get(
cm.getLineNumber(cm.extras().activeLine()) + 1);
if (w != null) {
w.openCloseAll();
}
}
}
};
}
@Override
Runnable newDraftCallback(final CodeMirror cm) {
if (!Gerrit.isSignedIn()) {
return new Runnable() {
@Override
public void run() {
String token = host.getToken();
if (cm.extras().hasActiveLine()) {
LineHandle handle = cm.extras().activeLine();
int line = cm.getLineNumber(handle) + 1;
token += "@" + (cm.side() == DisplaySide.A ? "a" : "") + line;
}
Gerrit.doSignIn(token);
}
};
}
return new Runnable() {
@Override
public void run() {
if (cm.extras().hasActiveLine()) {
newDraft(cm);
}
}
};
}
private void newDraft(CodeMirror cm) {
int line = cm.getLineNumber(cm.extras().activeLine()) + 1;
if (cm.somethingSelected()) {
FromTo fromTo = cm.getSelectedRange();
Pos end = fromTo.to();
if (end.ch() == 0) {
end.line(end.line() - 1);
end.ch(cm.getLine(end.line()).length());
}
addDraftBox(cm.side(), CommentInfo.create(
getPath(),
getStoredSideFromDisplaySide(cm.side()),
line,
CommentRange.create(fromTo))).setEdit(true);
cm.setSelection(cm.getCursor());
} else {
insertNewDraft(cm.side(), line);
}
}
private SideBySideCommentGroup group(DisplaySide side, int line) {
SideBySideCommentGroup w = map(side).get(line);
if (w != null) {
return w;
}
int lineA, lineB;
if (line == 0) {
lineA = lineB = 0;
} else if (side == DisplaySide.A) {
lineA = line;
lineB = host.lineOnOther(side, line - 1).getLine() + 1;
} else {
lineA = host.lineOnOther(side, line - 1).getLine() + 1;
lineB = line;
}
SideBySideCommentGroup a = newGroup(DisplaySide.A, lineA);
SideBySideCommentGroup b = newGroup(DisplaySide.B, lineB);
SideBySideCommentGroup.pair(a, b);
sideA.put(lineA, a);
sideB.put(lineB, b);
if (isAttached()) {
a.init(host.getDiffTable());
b.handleRedraw();
}
return side == DisplaySide.A ? a : b;
}
private SideBySideCommentGroup newGroup(DisplaySide side, int line) {
return new SideBySideCommentGroup(this, host.getCmFromSide(side), side, line);
}
private SortedMap<Integer, SideBySideCommentGroup> map(DisplaySide side) {
return side == DisplaySide.A ? sideA : sideB;
}
}

View File

@@ -0,0 +1,217 @@
// 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.patches.PatchUtil;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.HTMLPanel;
import net.codemirror.lib.CodeMirror;
import net.codemirror.lib.Configuration;
import net.codemirror.lib.LineWidget;
import net.codemirror.lib.Pos;
import net.codemirror.lib.TextMarker;
import net.codemirror.lib.TextMarker.FromTo;
/** The Widget that handles expanding of skipped lines */
class SideBySideSkipBar extends SkipBar {
interface Binder extends UiBinder<HTMLPanel, SideBySideSkipBar> {}
private static final Binder uiBinder = GWT.create(Binder.class);
private static final int NUM_ROWS_TO_EXPAND = 10;
private static final int UP_DOWN_THRESHOLD = 30;
interface SkipBarStyle extends CssResource {
String noExpand();
}
@UiField(provided=true) Anchor skipNum;
@UiField(provided=true) Anchor upArrow;
@UiField(provided=true) Anchor downArrow;
@UiField SkipBarStyle style;
private final SideBySideSkipManager manager;
private final CodeMirror cm;
private LineWidget lineWidget;
private TextMarker textMarker;
private SideBySideSkipBar otherBar;
SideBySideSkipBar(SideBySideSkipManager manager, final CodeMirror cm) {
this.manager = manager;
this.cm = cm;
skipNum = new Anchor(true);
upArrow = new Anchor(true);
downArrow = new Anchor(true);
initWidget(uiBinder.createAndBindUi(this));
addDomHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
cm.focus();
}
}, ClickEvent.getType());
}
void collapse(int start, int end, boolean attach) {
if (attach) {
boolean isNew = lineWidget == null;
Configuration cfg = Configuration.create()
.set("coverGutter", true)
.set("noHScroll", true);
if (start == 0) { // First line workaround
lineWidget = cm.addLineWidget(end + 1, getElement(), cfg.set("above", true));
} else {
lineWidget = cm.addLineWidget(start - 1, getElement(), cfg);
}
if (isNew) {
lineWidget.onFirstRedraw(new Runnable() {
@Override
public void run() {
int w = cm.getGutterElement().getOffsetWidth();
getElement().getStyle().setPaddingLeft(w, Unit.PX);
}
});
}
}
textMarker = cm.markText(
Pos.create(start, 0),
Pos.create(end),
Configuration.create()
.set("collapsed", true)
.set("inclusiveLeft", true)
.set("inclusiveRight", true));
textMarker.on("beforeCursorEnter", new Runnable() {
@Override
public void run() {
expandAll();
}
});
int skipped = end - start + 1;
if (skipped <= UP_DOWN_THRESHOLD) {
addStyleName(style.noExpand());
} else {
upArrow.setHTML(PatchUtil.M.expandBefore(NUM_ROWS_TO_EXPAND));
downArrow.setHTML(PatchUtil.M.expandAfter(NUM_ROWS_TO_EXPAND));
}
skipNum.setText(PatchUtil.M.patchSkipRegion(Integer
.toString(skipped)));
}
static void link(SideBySideSkipBar barA, SideBySideSkipBar barB) {
barA.otherBar = barB;
barB.otherBar = barA;
}
private void clearMarkerAndWidget() {
textMarker.clear();
lineWidget.clear();
}
@Override
void expandBefore(int cnt) {
expandSideBefore(cnt);
if (otherBar != null) {
otherBar.expandSideBefore(cnt);
}
}
private void expandSideBefore(int cnt) {
FromTo range = textMarker.find();
int oldStart = range.from().line();
int newStart = oldStart + cnt;
int end = range.to().line();
clearMarkerAndWidget();
collapse(newStart, end, true);
updateSelection();
}
@Override
void expandSideAll() {
clearMarkerAndWidget();
removeFromParent();
}
private void expandAfter() {
FromTo range = textMarker.find();
int start = range.from().line();
int oldEnd = range.to().line();
int newEnd = oldEnd - NUM_ROWS_TO_EXPAND;
boolean attach = start == 0;
if (attach) {
clearMarkerAndWidget();
} else {
textMarker.clear();
}
collapse(start, newEnd, attach);
updateSelection();
}
private void updateSelection() {
if (cm.somethingSelected()) {
FromTo sel = cm.getSelectedRange();
cm.setSelection(sel.from(), sel.to());
}
}
@UiHandler("skipNum")
void onExpandAll(@SuppressWarnings("unused") ClickEvent e) {
expandAll();
updateSelection();
if (otherBar != null) {
otherBar.expandAll();
otherBar.updateSelection();
}
cm.focus();
}
private void expandAll() {
expandSideAll();
if (otherBar != null) {
otherBar.expandSideAll();
}
manager.remove(this, otherBar);
}
@UiHandler("upArrow")
void onExpandBefore(@SuppressWarnings("unused") ClickEvent e) {
expandBefore(NUM_ROWS_TO_EXPAND);
if (otherBar != null) {
otherBar.expandBefore(NUM_ROWS_TO_EXPAND);
}
cm.focus();
}
@UiHandler("downArrow")
void onExpandAfter(@SuppressWarnings("unused") ClickEvent e) {
expandAfter();
if (otherBar != null) {
otherBar.expandAfter();
}
cm.focus();
}
}

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
xmlns:g='urn:import:com.google.gwt.user.client.ui'>
<ui:style gss='false' type='com.google.gerrit.client.diff.SideBySideSkipBar.SkipBarStyle'>
.skipBar {
background-color: #def;
height: 1.3em;
overflow: hidden;
}
.text {
display: table;
margin: 0 auto;
color: #777;
font-style: italic;
overflow: hidden;
}
.anchor {
color: inherit;
text-decoration: none;
}
.noExpand .arrow {
display: none;
}
.arrow {
font-family: Arial Unicode MS, sans-serif;
}
</ui:style>
<g:HTMLPanel addStyleNames='{style.skipBar}'>
<div class='{style.text}'>
<ui:msg>
<g:Anchor ui:field='upArrow' addStyleNames='{style.arrow} {style.anchor}' />
<g:Anchor ui:field='skipNum' addStyleNames='{style.anchor}' />
<g:Anchor ui:field='downArrow' addStyleNames=' {style.arrow} {style.anchor}' />
</ui:msg>
</div>
</g:HTMLPanel>
</ui:UiBinder>

View File

@@ -0,0 +1,85 @@
// 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.patches.SkippedLine;
import net.codemirror.lib.CodeMirror;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/** Collapses common regions with {@link SideBySideSkipBar} for {@link SideBySide}. */
class SideBySideSkipManager extends SkipManager {
private SideBySide host;
SideBySideSkipManager(SideBySide host, SideBySideCommentManager commentManager) {
super(commentManager);
this.host = host;
}
@Override
void render(int context, DiffInfo diff) {
List<SkippedLine> skips = getSkippedLines(context, diff);
if (!skips.isEmpty()) {
CodeMirror cmA = host.getCmFromSide(DisplaySide.A);
CodeMirror cmB = host.getCmFromSide(DisplaySide.B);
Set<SkipBar> skipBars = new HashSet<>();
setSkipBars(skipBars);
for (SkippedLine skip : skips) {
SideBySideSkipBar barA = newSkipBar(cmA, DisplaySide.A, skip);
SideBySideSkipBar barB = newSkipBar(cmB, DisplaySide.B, skip);
SideBySideSkipBar.link(barA, barB);
skipBars.add(barA);
skipBars.add(barB);
if (skip.getStartA() == 0 || skip.getStartB() == 0) {
barA.upArrow.setVisible(false);
barB.upArrow.setVisible(false);
setLine0(barB);
} else if (skip.getStartA() + skip.getSize() == getLineA()
|| skip.getStartB() + skip.getSize() == getLineB()) {
barA.downArrow.setVisible(false);
barB.downArrow.setVisible(false);
}
}
}
}
void remove(SideBySideSkipBar a, SideBySideSkipBar b) {
Set<SkipBar> skipBars = getSkipBars();
skipBars.remove(a);
skipBars.remove(b);
if (getLine0() == a || getLine0() == b) {
setLine0(null);
}
if (skipBars.isEmpty()) {
setSkipBars(null);
}
}
private SideBySideSkipBar newSkipBar(CodeMirror cm, DisplaySide side, SkippedLine skip) {
int start = side == DisplaySide.A ? skip.getStartA() : skip.getStartB();
int end = start + skip.getSize() - 1;
SideBySideSkipBar bar = new SideBySideSkipBar(this, cm);
host.getDiffTable().add(bar);
bar.collapse(start, end, true);
return bar;
}
}

View File

@@ -0,0 +1,105 @@
// 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.reviewdb.client.Patch.ChangeType;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.HTMLPanel;
/**
* A table with one row and two columns to hold the two CodeMirrors displaying
* the files to be compared.
*/
class SideBySideTable extends DiffTable {
interface Binder extends UiBinder<HTMLPanel, SideBySideTable> {}
private static final Binder uiBinder = GWT.create(Binder.class);
interface DiffTableStyle extends CssResource {
String intralineBg();
String diff();
String showLineNumbers();
String hideA();
String hideB();
String padding();
}
private SideBySide parent;
@UiField Element cmA;
@UiField Element cmB;
@UiField static DiffTableStyle style;
private boolean visibleA;
SideBySideTable(SideBySide parent, PatchSet.Id base, PatchSet.Id revision,
String path) {
super(parent, base, revision, path);
initWidget(uiBinder.createAndBindUi(this));
this.visibleA = true;
this.parent = parent;
}
@Override
boolean isVisibleA() {
return visibleA;
}
void setVisibleA(boolean show) {
visibleA = show;
if (show) {
removeStyleName(style.hideA());
parent.syncScroll(DisplaySide.B); // match B's viewport
} else {
addStyleName(style.hideA());
}
}
Runnable toggleA() {
return new Runnable() {
@Override
public void run() {
setVisibleA(!isVisibleA());
}
};
}
void setVisibleB(boolean show) {
if (show) {
removeStyleName(style.hideB());
parent.syncScroll(DisplaySide.A); // match A's viewport
} else {
addStyleName(style.hideB());
}
}
@Override
void setHideEmptyPane(boolean hide) {
if (getChangeType() == ChangeType.ADDED) {
setVisibleA(!hide);
} else if (getChangeType() == ChangeType.DELETED) {
setVisibleB(!hide);
}
}
@Override
SideBySide getDiffScreen() {
return parent;
}
}

View File

@@ -17,17 +17,15 @@ limitations under the License.
<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
xmlns:g='urn:import:com.google.gwt.user.client.ui'
xmlns:d='urn:import:com.google.gerrit.client.diff'>
<ui:style gss='false' type='com.google.gerrit.client.diff.DiffTable.DiffTableStyle'>
<ui:with field='res' type='com.google.gerrit.client.diff.Resources'/>
<ui:style gss='false' type='com.google.gerrit.client.diff.SideBySideTable.DiffTableStyle'>
@external .CodeMirror, .CodeMirror-selectedtext;
@external .CodeMirror-linenumber;
@external .CodeMirror-overlayscroll-vertical, .CodeMirror-scroll;
@external .CodeMirror-dialog-bottom;
@external .CodeMirror-cursor;
.fullscreen {
background-color: #f7f7f7;
border-bottom: 1px solid #ddd;
}
@external .dark, .noIntraline;
.difftable .patchSetNav,
.difftable .CodeMirror {
@@ -98,16 +96,7 @@ limitations under the License.
background-color: #f7f7f7;
line-height: 1;
}
.fileCommentCell {
overflow-x: auto;
}
.range {
background-color: #ffd500 !important;
}
.rangeHighlight {
background-color: #ffff00 !important;
}
.difftable .CodeMirror-selectedtext {
background-color: inherit !important;
}
@@ -132,15 +121,6 @@ limitations under the License.
margin-left: 21px;
border-left: 2px solid #d64040;
}
.diff_header {
font-size: 12px;
font-weight: bold;
color: #5252ad;
}
.diff_header pre {
margin: 0 0 3px 0;
}
</ui:style>
<g:HTMLPanel styleName='{style.difftable}'>
<table class='{style.table}'>
@@ -152,7 +132,7 @@ limitations under the License.
<d:PatchSetSelectBox ui:field='patchSetSelectBoxB' />
</td>
</tr>
<tr ui:field='diffHeaderRow' class='{style.diff_header}'>
<tr ui:field='diffHeaderRow' class='{res.diffTableStyle.diffHeader}'>
<td colspan='2'><pre ui:field='diffHeaderText' /></td>
</tr>
<tr>

View File

@@ -14,189 +14,9 @@
package com.google.gerrit.client.diff;
import com.google.gerrit.client.patches.PatchUtil;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HTMLPanel;
import net.codemirror.lib.CodeMirror;
import net.codemirror.lib.Configuration;
import net.codemirror.lib.LineWidget;
import net.codemirror.lib.Pos;
import net.codemirror.lib.TextMarker;
import net.codemirror.lib.TextMarker.FromTo;
/** The Widget that handles expanding of skipped lines */
class SkipBar extends Composite {
interface Binder extends UiBinder<HTMLPanel, SkipBar> {}
private static final Binder uiBinder = GWT.create(Binder.class);
private static final int NUM_ROWS_TO_EXPAND = 10;
private static final int UP_DOWN_THRESHOLD = 30;
interface SkipBarStyle extends CssResource {
String noExpand();
}
@UiField(provided=true) Anchor skipNum;
@UiField(provided=true) Anchor upArrow;
@UiField(provided=true) Anchor downArrow;
@UiField SkipBarStyle style;
private final SkipManager manager;
private final CodeMirror cm;
private LineWidget lineWidget;
private TextMarker textMarker;
private SkipBar otherBar;
SkipBar(SkipManager manager, final CodeMirror cm) {
this.manager = manager;
this.cm = cm;
skipNum = new Anchor(true);
upArrow = new Anchor(true);
downArrow = new Anchor(true);
initWidget(uiBinder.createAndBindUi(this));
addDomHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
cm.focus();
}
}, ClickEvent.getType());
}
void collapse(int start, int end, boolean attach) {
if (attach) {
boolean isNew = lineWidget == null;
Configuration cfg = Configuration.create()
.set("coverGutter", true)
.set("noHScroll", true);
if (start == 0) { // First line workaround
lineWidget = cm.addLineWidget(end + 1, getElement(), cfg.set("above", true));
} else {
lineWidget = cm.addLineWidget(start - 1, getElement(), cfg);
}
if (isNew) {
lineWidget.onFirstRedraw(new Runnable() {
@Override
public void run() {
int w = cm.getGutterElement().getOffsetWidth();
getElement().getStyle().setPaddingLeft(w, Unit.PX);
}
});
}
}
textMarker = cm.markText(
Pos.create(start, 0),
Pos.create(end),
Configuration.create()
.set("collapsed", true)
.set("inclusiveLeft", true)
.set("inclusiveRight", true));
textMarker.on("beforeCursorEnter", new Runnable() {
@Override
public void run() {
expandAll();
}
});
int skipped = end - start + 1;
if (skipped <= UP_DOWN_THRESHOLD) {
addStyleName(style.noExpand());
} else {
upArrow.setHTML(PatchUtil.M.expandBefore(NUM_ROWS_TO_EXPAND));
downArrow.setHTML(PatchUtil.M.expandAfter(NUM_ROWS_TO_EXPAND));
}
skipNum.setText(PatchUtil.M.patchSkipRegion(Integer
.toString(skipped)));
}
static void link(SkipBar barA, SkipBar barB) {
barA.otherBar = barB;
barB.otherBar = barA;
}
private void clearMarkerAndWidget() {
textMarker.clear();
lineWidget.clear();
}
void expandBefore(int cnt) {
expandSideBefore(cnt);
otherBar.expandSideBefore(cnt);
}
private void expandSideBefore(int cnt) {
FromTo range = textMarker.find();
int oldStart = range.from().line();
int newStart = oldStart + cnt;
int end = range.to().line();
clearMarkerAndWidget();
collapse(newStart, end, true);
updateSelection();
}
void expandSideAll() {
clearMarkerAndWidget();
removeFromParent();
}
private void expandAfter() {
FromTo range = textMarker.find();
int start = range.from().line();
int oldEnd = range.to().line();
int newEnd = oldEnd - NUM_ROWS_TO_EXPAND;
boolean attach = start == 0;
if (attach) {
clearMarkerAndWidget();
} else {
textMarker.clear();
}
collapse(start, newEnd, attach);
updateSelection();
}
private void updateSelection() {
if (cm.somethingSelected()) {
FromTo sel = cm.getSelectedRange();
cm.setSelection(sel.from(), sel.to());
}
}
@UiHandler("skipNum")
void onExpandAll(@SuppressWarnings("unused") ClickEvent e) {
expandAll();
updateSelection();
otherBar.updateSelection();
cm.focus();
}
private void expandAll() {
expandSideAll();
otherBar.expandSideAll();
manager.remove(this, otherBar);
}
@UiHandler("upArrow")
void onExpandBefore(@SuppressWarnings("unused") ClickEvent e) {
expandBefore(NUM_ROWS_TO_EXPAND);
cm.focus();
}
@UiHandler("downArrow")
void onExpandAfter(@SuppressWarnings("unused") ClickEvent e) {
expandAfter();
otherBar.expandAfter();
cm.focus();
}
abstract class SkipBar extends Composite {
abstract void expandSideAll();
abstract void expandBefore(int cnt);
}

View File

@@ -19,34 +19,34 @@ import com.google.gerrit.client.patches.SkippedLine;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gwt.core.client.JsArray;
import net.codemirror.lib.CodeMirror;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/** Collapses common regions with {@link SkipBar} for {@link SideBySide}. */
class SkipManager {
private final SideBySide host;
private final CommentManager commentManager;
/** Collapses common regions with {@link SideBySideSkipBar} for {@link SideBySide}
* and {@link Unified}. */
abstract class SkipManager {
private Set<SkipBar> skipBars;
private SkipBar line0;
private CommentManager commentManager;
private int lineA;
private int lineB;
SkipManager(SideBySide host, CommentManager commentManager) {
this.host = host;
SkipManager(CommentManager commentManager) {
this.commentManager = commentManager;
}
void render(int context, DiffInfo diff) {
abstract void render(int context, DiffInfo diff);
List<SkippedLine> getSkippedLines(int context, DiffInfo diff) {
if (context == DiffPreferencesInfo.WHOLE_FILE_CONTEXT) {
return;
return new ArrayList<>();
}
lineA = 0;
lineB = 0;
JsArray<Region> regions = diff.content();
List<SkippedLine> skips = new ArrayList<>();
int lineA = 0;
int lineB = 0;
for (int i = 0; i < regions.length(); i++) {
Region current = regions.get(i);
if (current.ab() != null || current.common() || current.skip() > 0) {
@@ -69,31 +69,7 @@ class SkipManager {
lineB += current.b() != null ? current.b().length() : 0;
}
}
skips = commentManager.splitSkips(context, skips);
if (!skips.isEmpty()) {
CodeMirror cmA = host.getCmFromSide(DisplaySide.A);
CodeMirror cmB = host.getCmFromSide(DisplaySide.B);
skipBars = new HashSet<>();
for (SkippedLine skip : skips) {
SkipBar barA = newSkipBar(cmA, DisplaySide.A, skip);
SkipBar barB = newSkipBar(cmB, DisplaySide.B, skip);
SkipBar.link(barA, barB);
skipBars.add(barA);
skipBars.add(barB);
if (skip.getStartA() == 0 || skip.getStartB() == 0) {
barA.upArrow.setVisible(false);
barB.upArrow.setVisible(false);
line0 = barB;
} else if (skip.getStartA() + skip.getSize() == lineA
|| skip.getStartB() + skip.getSize() == lineB) {
barA.downArrow.setVisible(false);
barB.downArrow.setVisible(false);
}
}
}
return commentManager.splitSkips(context, skips);
}
void ensureFirstLineIsVisible() {
@@ -113,24 +89,27 @@ class SkipManager {
}
}
void remove(SkipBar a, SkipBar b) {
skipBars.remove(a);
skipBars.remove(b);
if (line0 == a || line0 == b) {
line0 = null;
}
if (skipBars.isEmpty()) {
skipBars = null;
}
SkipBar getLine0() {
return line0;
}
private SkipBar newSkipBar(CodeMirror cm, DisplaySide side, SkippedLine skip) {
int start = side == DisplaySide.A ? skip.getStartA() : skip.getStartB();
int end = start + skip.getSize() - 1;
int getLineA() {
return lineA;
}
SkipBar bar = new SkipBar(this, cm);
host.diffTable.add(bar);
bar.collapse(start, end, true);
return bar;
int getLineB() {
return lineB;
}
void setLine0(SkipBar bar) {
line0 = bar;
}
void setSkipBars(Set<SkipBar> bars) {
skipBars = bars;
}
Set<SkipBar> getSkipBars() {
return skipBars;
}
}

View File

@@ -0,0 +1,427 @@
// 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.lang.Double.POSITIVE_INFINITY;
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.account.DiffPreferences;
import com.google.gerrit.client.diff.UnifiedChunkManager.LineSidePair;
import com.google.gerrit.client.patches.PatchUtil;
import com.google.gerrit.client.projects.ConfigInfoCache;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.InlineHyperlink;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.ImageResourceRenderer;
import com.google.gwt.user.client.ui.Label;
import com.google.gwtexpui.globalkey.client.GlobalKey;
import net.codemirror.lib.CodeMirror;
import net.codemirror.lib.CodeMirror.GutterClickHandler;
import net.codemirror.lib.CodeMirror.LineHandle;
import net.codemirror.lib.Configuration;
import net.codemirror.lib.Pos;
import net.codemirror.lib.ScrollInfo;
import java.util.Collections;
import java.util.List;
public class Unified extends DiffScreen {
interface Binder extends UiBinder<FlowPanel, Unified> {}
private static final Binder uiBinder = GWT.create(Binder.class);
@UiField(provided = true)
UnifiedTable diffTable;
private CodeMirror cm;
private UnifiedChunkManager chunkManager;
private UnifiedCommentManager commentManager;
private UnifiedSkipManager skipManager;
private boolean autoHideDiffTableHeader;
public Unified(
PatchSet.Id base,
PatchSet.Id revision,
String path,
DisplaySide startSide,
int startLine) {
super(base, revision, path, startSide, startLine, DiffScreenType.UNIFIED);
diffTable = new UnifiedTable(this, base, revision, path);
add(uiBinder.createAndBindUi(this));
addDomHandler(GlobalKey.STOP_PROPAGATION, KeyPressEvent.getType());
}
@Override
ScreenLoadCallback<ConfigInfoCache.Entry> getScreenLoadCallback(
final CommentsCollections comments) {
return new ScreenLoadCallback<ConfigInfoCache.Entry>(Unified.this) {
@Override
protected void preDisplay(ConfigInfoCache.Entry result) {
commentManager = new UnifiedCommentManager(
Unified.this,
getBase(), getRevision(), getPath(),
result.getCommentLinkProcessor(),
getChangeStatus().isOpen());
setTheme(result.getTheme());
display(comments);
}
};
}
@Override
public void onShowView() {
super.onShowView();
operation(new Runnable() {
@Override
public void run() {
resizeCodeMirror();
cm.refresh();
}
});
setLineLength(Patch.COMMIT_MSG.equals(getPath()) ? 72 : getPrefs().lineLength());
diffTable.refresh();
if (getStartLine() == 0) {
DiffChunkInfo d = chunkManager.getFirst();
if (d != null) {
if (d.isEdit() && d.getSide() == DisplaySide.A) {
setStartSide(DisplaySide.B);
} else {
setStartSide(d.getSide());
}
setStartLine(chunkManager.getCmLine(d.getStart(), d.getSide()) + 1);
}
}
if (getStartSide() != null && getStartLine() > 0) {
cm.scrollToLine(
chunkManager.getCmLine(getStartLine() - 1, getStartSide()));
cm.focus();
} else {
cm.setCursor(Pos.create(0));
cm.focus();
}
if (Gerrit.isSignedIn() && getPrefs().autoReview()) {
header.autoReview();
}
prefetchNextFile();
}
@Override
void registerCmEvents(final CodeMirror cm) {
super.registerCmEvents(cm);
cm.on("scroll", new Runnable() {
@Override
public void run() {
ScrollInfo si = cm.getScrollInfo();
if (autoHideDiffTableHeader) {
updateDiffTableHeader(si);
}
}
});
maybeRegisterRenderEntireFileKeyMap(cm);
}
@Override
public void registerKeys() {
super.registerKeys();
registerHandlers();
}
@Override
FocusHandler getFocusHandler() {
return new FocusHandler() {
@Override
public void onFocus(FocusEvent event) {
cm.focus();
}
};
}
private void display(final CommentsCollections comments) {
final DiffPreferences prefs = getPrefs();
final DiffInfo diff = getDiff();
setThemeStyles(prefs.theme().isDark());
setShowIntraline(prefs.intralineDifference());
// TODO: Handle showLineNumbers preference
cm = newCm(
diff.metaA() == null ? diff.metaB() : diff.metaA(),
diff.textUnified(),
diffTable.cm);
setShowTabs(prefs.showTabs());
chunkManager = new UnifiedChunkManager(this, cm, diffTable.scrollbar);
skipManager = new UnifiedSkipManager(this, commentManager);
operation(new Runnable() {
@Override
public void run() {
// Estimate initial CM3 height, fixed up in onShowView.
int height = Window.getClientHeight()
- (Gerrit.getHeaderFooterHeight() + 18);
cm.setHeight(height);
render(diff);
commentManager.render(comments, prefs.expandAllComments());
skipManager.render(prefs.context(), diff);
}
});
registerCmEvents(cm);
setPrefsAction(new PreferencesAction(this, prefs));
header.init(getPrefsAction(), getSideBySideDiffLink(), diff.sideBySideWebLinks());
setAutoHideDiffHeader(prefs.autoHideDiffTableHeader());
setupSyntaxHighlighting();
}
private List<InlineHyperlink> getSideBySideDiffLink() {
InlineHyperlink toSideBySideDiffLink = new InlineHyperlink();
toSideBySideDiffLink.setHTML(
new ImageResourceRenderer().render(Gerrit.RESOURCES.sideBySideDiff()));
toSideBySideDiffLink.setTargetHistoryToken(
Dispatcher.toSideBySide(getBase(), getRevision(), getPath()));
toSideBySideDiffLink.setTitle(PatchUtil.C.sideBySideDiff());
return Collections.singletonList(toSideBySideDiffLink);
}
@Override
CodeMirror newCm(
DiffInfo.FileMeta meta,
String contents,
Element parent) {
DiffPreferences prefs = getPrefs();
JsArrayString gutters = JavaScriptObject.createArray().cast();
gutters.push(UnifiedTable.style.lineNumbersLeft());
gutters.push(UnifiedTable.style.lineNumbersRight());
return CodeMirror.create(parent, Configuration.create()
.set("cursorBlinkRate", prefs.cursorBlinkRate())
.set("cursorHeight", 0.85)
.set("gutters", gutters)
.set("keyMap", "vim_ro")
.set("lineNumbers", false)
.set("lineWrapping", false)
.set("matchBrackets", prefs.matchBrackets())
.set("mode", getFileSize() == FileSize.SMALL ? getContentType(meta) : null)
.set("readOnly", true)
.set("scrollbarStyle", "overlay")
.set("styleSelectedText", true)
.set("showTrailingSpace", prefs.showWhitespaceErrors())
.set("tabSize", prefs.tabSize())
.set("theme", prefs.theme().name().toLowerCase())
.set("value", meta != null ? contents : "")
.set("viewportMargin", renderEntireFile() ? POSITIVE_INFINITY : 10));
}
@Override
void setShowLineNumbers(boolean b) {
// TODO: Implement this
}
private GutterClickHandler onGutterClick(final int cmLine) {
return new GutterClickHandler() {
@Override
public void handle(CodeMirror instance, int line, String gutter,
NativeEvent clickEvent) {
if (clickEvent.getButton() == NativeEvent.BUTTON_LEFT
&& !clickEvent.getMetaKey()
&& !clickEvent.getAltKey()
&& !clickEvent.getCtrlKey()
&& !clickEvent.getShiftKey()) {
cm.setCursor(Pos.create(cmLine));
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
commentManager.newDraftCallback(cm).run();
}
});
}
}
};
}
LineHandle setLineNumber(DisplaySide side, final int cmLine, int line) {
Label gutter = new Label(String.valueOf(line));
gutter.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
onGutterClick(cmLine);
}
});
diffTable.add(gutter);
gutter.setStyleName(UnifiedTable.style.unifiedLineNumber());
return cm.setGutterMarker(cmLine,
side == DisplaySide.A ? UnifiedTable.style.lineNumbersLeft()
: UnifiedTable.style.lineNumbersRight(), gutter.getElement());
}
@Override
void setSyntaxHighlighting(boolean b) {
final DiffInfo diff = getDiff();
final DiffPreferences prefs = getPrefs();
if (b) {
injectMode(diff, new AsyncCallback<Void>() {
@Override
public void onSuccess(Void result) {
if (prefs.syntaxHighlighting()) {
cm.setOption("mode", getContentType(diff.metaA() == null
? diff.metaB()
: diff.metaA()));
}
}
@Override
public void onFailure(Throwable caught) {
prefs.syntaxHighlighting(false);
}
});
} else {
cm.setOption("mode", (String) null);
}
}
@Override
void setAutoHideDiffHeader(boolean autoHide) {
if (autoHide) {
updateDiffTableHeader(cm.getScrollInfo());
} else {
diffTable.setHeaderVisible(true);
}
autoHideDiffTableHeader = autoHide;
}
private void updateDiffTableHeader(ScrollInfo si) {
if (si.top() == 0) {
diffTable.setHeaderVisible(true);
} else if (si.top() > 0.5 * si.clientHeight()) {
diffTable.setHeaderVisible(false);
}
}
@Override
Runnable updateActiveLine(final CodeMirror cm) {
return new Runnable() {
@Override
public void run() {
// The rendering of active lines has to be deferred. Reflow
// caused by adding and removing styles chokes Firefox when arrow
// key (or j/k) is held down. Performance on Chrome is fine
// without the deferral.
//
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
LineHandle handle =
cm.getLineHandleVisualStart(cm.getCursor("end").line());
cm.extras().activeLine(handle);
}
});
}
};
}
@Override
CodeMirror getCmFromSide(DisplaySide side) {
return cm;
}
int getCmLine(int line, DisplaySide side) {
return chunkManager.getCmLine(line, side);
}
LineSidePair getLineSidePairFromCmLine(int cmLine) {
return chunkManager.getLineSidePairFromCmLine(cmLine);
}
@Override
void resizeCodeMirror() {
int hdr = header.getOffsetHeight() + diffTable.getHeaderHeight();
cm.adjustHeight(hdr);
}
@Override
void operation(final Runnable apply) {
cm.operation(new Runnable() {
@Override
public void run() {
apply.run();
}
});
}
@Override
int getCodeMirrorHeight() {
int rest =
Gerrit.getHeaderFooterHeight() + header.getOffsetHeight()
+ diffTable.getHeaderHeight() + 5; // Estimate
return Window.getClientHeight() - rest;
}
@Override
CodeMirror[] getCms() {
return new CodeMirror[] {cm};
}
CodeMirror getCm() {
return cm;
}
@Override
UnifiedTable getDiffTable() {
return diffTable;
}
@Override
UnifiedChunkManager getChunkManager() {
return chunkManager;
}
@Override
UnifiedCommentManager getCommentManager() {
return commentManager;
}
@Override
UnifiedSkipManager getSkipManager() {
return skipManager;
}
}

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
xmlns:g='urn:import:com.google.gwt.user.client.ui'
xmlns:d='urn:import:com.google.gerrit.client.diff'>
<ui:style>
.unified {
margin-left: -5px;
margin-right: -5px;
}
</ui:style>
<g:FlowPanel styleName='{style.unified}'>
<d:Header ui:field='header'/>
<d:UnifiedTable ui:field='diffTable'/>
</g:FlowPanel>
</ui:UiBinder>

View File

@@ -0,0 +1,316 @@
// 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;
}
}
}

View File

@@ -0,0 +1,89 @@
// Copyright (C) 2014 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.gwt.user.client.Timer;
import net.codemirror.lib.CodeMirror;
/**
* LineWidget attached to a CodeMirror container.
*
* When a comment is placed on a line a CommentWidget is created.
* The group tracks all comment boxes on a line in unified diff view.
*/
class UnifiedCommentGroup extends CommentGroup {
UnifiedCommentGroup(UnifiedCommentManager manager, CodeMirror cm, DisplaySide side, int line) {
super(manager, cm, side, line);
}
@Override
void remove(DraftBox box) {
super.remove(box);
if (0 < getBoxCount()) {
resize();
} else {
detach();
}
}
@Override
void init(DiffTable parent) {
if (getLineWidget() == null) {
attach(parent);
}
}
@Override
void handleRedraw() {
getLineWidget().onRedraw(new Runnable() {
@Override
public void run() {
if (canComputeHeight()) {
if (getResizeTimer() != null) {
getResizeTimer().cancel();
setResizeTimer(null);
}
reportHeightChange();
} else if (getResizeTimer() == null) {
setResizeTimer(new Timer() {
@Override
public void run() {
if (canComputeHeight()) {
cancel();
setResizeTimer(null);
reportHeightChange();
}
}
});
getResizeTimer().scheduleRepeating(5);
}
}
});
}
@Override
void resize() {
if (getLineWidget() != null) {
reportHeightChange();
}
}
private void reportHeightChange() {
getLineWidget().changed();
updateSelection();
}
}

View File

@@ -0,0 +1,394 @@
// Copyright (C) 2014 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.Gerrit;
import com.google.gerrit.client.changes.CommentInfo;
import com.google.gerrit.client.diff.UnifiedChunkManager.LineSidePair;
import com.google.gerrit.client.patches.SkippedLine;
import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.ui.CommentLinkProcessor;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.JsArray;
import net.codemirror.lib.CodeMirror;
import net.codemirror.lib.CodeMirror.LineHandle;
import net.codemirror.lib.Pos;
import net.codemirror.lib.TextMarker.FromTo;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.TreeSet;
/** Tracks comment widgets for {@link Unified}. */
class UnifiedCommentManager extends CommentManager {
private final Unified host;
private final SortedMap<Integer, UnifiedCommentGroup> sideA;
private final SortedMap<Integer, UnifiedCommentGroup> sideB;
UnifiedCommentManager(Unified host,
PatchSet.Id base, PatchSet.Id revision,
String path,
CommentLinkProcessor clp,
boolean open) {
super(base, revision, path, clp, open);
this.host = host;
sideA = new TreeMap<>();
sideB = new TreeMap<>();
}
@Override
Unified getDiffScreen() {
return host;
}
@Override
void setExpandAllComments(boolean b) {
setExpandAll(b);
for (UnifiedCommentGroup g : sideA.values()) {
g.setOpenAll(b);
}
for (UnifiedCommentGroup g : sideB.values()) {
g.setOpenAll(b);
}
}
@Override
Runnable commentNav(final CodeMirror src, final Direction dir) {
return new Runnable() {
@Override
public void run() {
SortedMap<Integer, UnifiedCommentGroup> map = map(src.side());
int line = src.extras().hasActiveLine()
? src.getLineNumber(src.extras().activeLine()) + 1
: 0;
if (dir == Direction.NEXT) {
map = map.tailMap(line + 1);
if (map.isEmpty()) {
return;
}
line = map.firstKey();
} else {
map = map.headMap(line);
if (map.isEmpty()) {
return;
}
line = map.lastKey();
}
UnifiedCommentGroup g = map.get(line);
CodeMirror cm = g.getCm();
double y = cm.heightAtLine(g.getLine() - 1, "local");
cm.setCursor(Pos.create(g.getLine() - 1));
cm.scrollToY(y - 0.5 * cm.scrollbarV().getClientHeight());
cm.focus();
}
};
}
void render(CommentsCollections in, boolean expandAll) {
if (in.publishedBase != null) {
renderPublished(DisplaySide.A, in.publishedBase);
}
if (in.publishedRevision != null) {
renderPublished(DisplaySide.B, in.publishedRevision);
}
if (in.draftsBase != null) {
renderDrafts(DisplaySide.A, in.draftsBase);
}
if (in.draftsRevision != null) {
renderDrafts(DisplaySide.B, in.draftsRevision);
}
if (expandAll) {
setExpandAllComments(true);
}
for (CommentGroup g : sideA.values()) {
g.init(host.getDiffTable());
}
for (CommentGroup g : sideB.values()) {
g.init(host.getDiffTable());
g.handleRedraw();
}
setAttached(true);
}
@Override
void renderPublished(DisplaySide forSide, JsArray<CommentInfo> in) {
for (CommentInfo info : Natives.asList(in)) {
DisplaySide side = displaySide(info, forSide);
if (side != null) {
int cmLinePlusOne = host.getCmLine(info.line() - 1, side);
UnifiedCommentGroup group = group(side, cmLinePlusOne);
PublishedBox box = new PublishedBox(
group,
getCommentLinkProcessor(),
getPatchSetIdFromSide(side),
info,
isOpen());
group.add(box);
box.setAnnotation(getDiffScreen().getDiffTable().scrollbar.comment(
host.getCm(),
cmLinePlusOne));
getPublished().put(info.id(), box);
}
}
}
@Override
void newDraftOnGutterClick(CodeMirror cm, String gutterClass, int cmLinePlusOne) {
DisplaySide side = gutterClass.equals(UnifiedTable.style.lineNumbersLeft())
? DisplaySide.A
: DisplaySide.B;
if (cm.somethingSelected()) {
FromTo fromTo = cm.getSelectedRange();
Pos end = fromTo.to();
if (end.ch() == 0) {
end.line(end.line() - 1);
end.ch(cm.getLine(end.line()).length());
}
LineSidePair pair = host.getLineSidePairFromCmLine(cmLinePlusOne - 1);
int line = pair.getLine();
if (pair.getSide() != side) {
line = host.lineOnOther(pair.getSide(), line).getLine();
}
addDraftBox(side, CommentInfo.create(
getPath(),
getStoredSideFromDisplaySide(side),
line + 1,
CommentRange.create(fromTo))).setEdit(true);
cm.setSelection(cm.getCursor());
} else {
insertNewDraft(side, cmLinePlusOne);
}
}
/**
* Create a new {@link DraftBox} at the specified line and focus it.
*
* @param side which side the draft will appear on.
* @param cmLinePlusOne the line the draft will be at, plus one.
* Lines are 1-based. Line 0 is a special case creating a file level comment.
*/
@Override
void insertNewDraft(DisplaySide side, int cmLinePlusOne) {
if (cmLinePlusOne == 0) {
getDiffScreen().getSkipManager().ensureFirstLineIsVisible();
}
CommentGroup group = group(side, cmLinePlusOne);
if (0 < group.getBoxCount()) {
CommentBox last = group.getCommentBox(group.getBoxCount() - 1);
if (last instanceof DraftBox) {
((DraftBox)last).setEdit(true);
} else {
((PublishedBox)last).doReply();
}
} else {
LineSidePair pair = host.getLineSidePairFromCmLine(cmLinePlusOne - 1);
int line = pair.getLine();
if (pair.getSide() != side) {
line = host.lineOnOther(pair.getSide(), line).getLine();
}
addDraftBox(side, CommentInfo.create(
getPath(),
getStoredSideFromDisplaySide(side),
line + 1,
null)).setEdit(true);
}
}
@Override
DraftBox addDraftBox(DisplaySide side, CommentInfo info) {
int cmLinePlusOne = host.getCmLine(info.line() - 1, side) + 1;
UnifiedCommentGroup group = group(side, cmLinePlusOne);
DraftBox box = new DraftBox(
group,
getCommentLinkProcessor(),
getPatchSetIdFromSide(side),
info,
isExpandAll());
if (info.inReplyTo() != null) {
PublishedBox r = getPublished().get(info.inReplyTo());
if (r != null) {
r.setReplyBox(box);
}
}
group.add(box);
box.setAnnotation(getDiffScreen().getDiffTable().scrollbar.draft(
host.getCm(),
cmLinePlusOne));
return box;
}
@Override
List<SkippedLine> splitSkips(int context, List<SkippedLine> skips) {
if (sideA.containsKey(0) || sideB.containsKey(0)) {
// Special case of file comment; cannot skip first line.
for (SkippedLine skip : skips) {
if (skip.getStartA() == 0) {
skip.incrementStart(1);
}
}
}
TreeSet<Integer> allBoxLines = new TreeSet<>(sideA.tailMap(1).keySet());
allBoxLines.addAll(sideB.tailMap(1).keySet());
for (int boxLine : allBoxLines) {
List<SkippedLine> temp = new ArrayList<>(skips.size() + 2);
for (SkippedLine skip : skips) {
int startLine = host.getCmLine(skip.getStartA(), DisplaySide.A);
int deltaBefore = boxLine - startLine;
int deltaAfter = startLine + skip.getSize() - boxLine;
if (deltaBefore < -context || deltaAfter < -context) {
temp.add(skip); // Size guaranteed to be greater than 1
} else if (deltaBefore > context && deltaAfter > context) {
SkippedLine before = new SkippedLine(
skip.getStartA(), skip.getStartB(),
skip.getSize() - deltaAfter - context);
skip.incrementStart(deltaBefore + context);
checkAndAddSkip(temp, before);
checkAndAddSkip(temp, skip);
} else if (deltaAfter > context) {
skip.incrementStart(deltaBefore + context);
checkAndAddSkip(temp, skip);
} else if (deltaBefore > context) {
skip.reduceSize(deltaAfter + context);
checkAndAddSkip(temp, skip);
}
}
if (temp.isEmpty()) {
return temp;
}
skips = temp;
}
return skips;
}
private static void checkAndAddSkip(List<SkippedLine> out, SkippedLine s) {
if (s.getSize() > 1) {
out.add(s);
}
}
@Override
void clearLine(DisplaySide side, int cmLinePlusOne, CommentGroup group) {
SortedMap<Integer, UnifiedCommentGroup> map = map(side);
if (map.get(cmLinePlusOne) == group) {
map.remove(cmLinePlusOne);
}
}
@Override
Runnable toggleOpenBox(final CodeMirror cm) {
return new Runnable() {
@Override
public void run() {
if (cm.extras().hasActiveLine()) {
UnifiedCommentGroup w = map(cm.side()).get(
cm.getLineNumber(cm.extras().activeLine()) + 1);
if (w != null) {
w.openCloseLast();
}
}
}
};
}
@Override
Runnable openCloseAll(final CodeMirror cm) {
return new Runnable() {
@Override
public void run() {
if (cm.extras().hasActiveLine()) {
CommentGroup w = map(cm.side()).get(
cm.getLineNumber(cm.extras().activeLine()) + 1);
if (w != null) {
w.openCloseAll();
}
}
}
};
}
@Override
Runnable newDraftCallback(final CodeMirror cm) {
if (!Gerrit.isSignedIn()) {
return new Runnable() {
@Override
public void run() {
String token = host.getToken();
if (cm.extras().hasActiveLine()) {
LineHandle handle = cm.extras().activeLine();
int line = cm.getLineNumber(handle) + 1;
token += "@" + line;
}
Gerrit.doSignIn(token);
}
};
}
return new Runnable() {
@Override
public void run() {
if (cm.extras().hasActiveLine()) {
newDraft(cm);
}
}
};
}
private void newDraft(CodeMirror cm) {
int cmLine = cm.getLineNumber(cm.extras().activeLine());
LineSidePair pair = host.getLineSidePairFromCmLine(cmLine);
DisplaySide side = pair.getSide();
if (cm.somethingSelected()) {
// TODO: Handle range comment
} else {
insertNewDraft(side, cmLine);
}
}
private UnifiedCommentGroup group(DisplaySide side, int cmLinePlusOne) {
UnifiedCommentGroup w = map(side).get(cmLinePlusOne);
if (w != null) {
return w;
}
UnifiedCommentGroup g = new UnifiedCommentGroup(this, host.getCm(), side, cmLinePlusOne);
if (side == DisplaySide.A) {
sideA.put(cmLinePlusOne, g);
} else {
sideB.put(cmLinePlusOne, g);
}
if (isAttached()) {
g.init(getDiffScreen().getDiffTable());
g.handleRedraw();
}
return g;
}
private SortedMap<Integer, UnifiedCommentGroup> map(DisplaySide side) {
return side == DisplaySide.A ? sideA : sideB;
}
}

View File

@@ -0,0 +1,30 @@
// 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;
public class UnifiedDiffChunkInfo extends DiffChunkInfo {
private int cmLine;
UnifiedDiffChunkInfo(DisplaySide side,
int start, int end, int cmLine, boolean edit) {
super(side, start, end, edit);
this.cmLine = cmLine;
}
int getCmLine() {
return cmLine;
}
}

View File

@@ -0,0 +1,193 @@
//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.patches.PatchUtil;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.HTMLPanel;
import net.codemirror.lib.CodeMirror;
import net.codemirror.lib.Configuration;
import net.codemirror.lib.LineWidget;
import net.codemirror.lib.Pos;
import net.codemirror.lib.TextMarker;
import net.codemirror.lib.TextMarker.FromTo;
/** The Widget that handles expanding of skipped lines */
class UnifiedSkipBar extends SkipBar {
interface Binder extends UiBinder<HTMLPanel, UnifiedSkipBar> {}
private static final Binder uiBinder = GWT.create(Binder.class);
private static final int NUM_ROWS_TO_EXPAND = 10;
private static final int UP_DOWN_THRESHOLD = 30;
interface SkipBarStyle extends CssResource {
String noExpand();
}
@UiField(provided=true) Anchor skipNum;
@UiField(provided=true) Anchor upArrow;
@UiField(provided=true) Anchor downArrow;
@UiField SkipBarStyle style;
private final UnifiedSkipManager manager;
private final CodeMirror cm;
private LineWidget lineWidget;
private TextMarker textMarker;
UnifiedSkipBar(UnifiedSkipManager manager, final CodeMirror cm) {
this.manager = manager;
this.cm = cm;
skipNum = new Anchor(true);
upArrow = new Anchor(true);
downArrow = new Anchor(true);
initWidget(uiBinder.createAndBindUi(this));
addDomHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
cm.focus();
}
}, ClickEvent.getType());
}
void collapse(int start, int end, boolean attach) {
if (attach) {
boolean isNew = lineWidget == null;
Configuration cfg = Configuration.create()
.set("coverGutter", true)
.set("noHScroll", true);
if (start == 0) { // First line workaround
lineWidget = cm.addLineWidget(end + 1, getElement(), cfg.set("above", true));
} else {
lineWidget = cm.addLineWidget(start - 1, getElement(), cfg);
}
if (isNew) {
lineWidget.onFirstRedraw(new Runnable() {
@Override
public void run() {
int w = cm.getGutterElement().getOffsetWidth();
getElement().getStyle().setPaddingLeft(w, Unit.PX);
}
});
}
}
textMarker = cm.markText(
Pos.create(start, 0),
Pos.create(end),
Configuration.create()
.set("collapsed", true)
.set("inclusiveLeft", true)
.set("inclusiveRight", true));
textMarker.on("beforeCursorEnter", new Runnable() {
@Override
public void run() {
expandAll();
}
});
int skipped = end - start + 1;
if (skipped <= UP_DOWN_THRESHOLD) {
addStyleName(style.noExpand());
} else {
upArrow.setHTML(PatchUtil.M.expandBefore(NUM_ROWS_TO_EXPAND));
downArrow.setHTML(PatchUtil.M.expandAfter(NUM_ROWS_TO_EXPAND));
}
skipNum.setText(PatchUtil.M.patchSkipRegion(Integer
.toString(skipped)));
}
private void clearMarkerAndWidget() {
textMarker.clear();
lineWidget.clear();
}
@Override
void expandBefore(int cnt) {
expandSideBefore(cnt);
}
private void expandSideBefore(int cnt) {
FromTo range = textMarker.find();
int oldStart = range.from().line();
int newStart = oldStart + cnt;
int end = range.to().line();
clearMarkerAndWidget();
collapse(newStart, end, true);
updateSelection();
}
@Override
void expandSideAll() {
clearMarkerAndWidget();
removeFromParent();
}
private void expandAfter() {
FromTo range = textMarker.find();
int start = range.from().line();
int oldEnd = range.to().line();
int newEnd = oldEnd - NUM_ROWS_TO_EXPAND;
boolean attach = start == 0;
if (attach) {
clearMarkerAndWidget();
} else {
textMarker.clear();
}
collapse(start, newEnd, attach);
updateSelection();
}
private void updateSelection() {
if (cm.somethingSelected()) {
FromTo sel = cm.getSelectedRange();
cm.setSelection(sel.from(), sel.to());
}
}
@UiHandler("skipNum")
void onExpandAll(@SuppressWarnings("unused") ClickEvent e) {
expandAll();
updateSelection();
cm.focus();
}
private void expandAll() {
expandSideAll();
manager.remove(this);
}
@UiHandler("upArrow")
void onExpandBefore(@SuppressWarnings("unused") ClickEvent e) {
expandBefore(NUM_ROWS_TO_EXPAND);
cm.focus();
}
@UiHandler("downArrow")
void onExpandAfter(@SuppressWarnings("unused") ClickEvent e) {
expandAfter();
cm.focus();
}
}

View File

@@ -16,7 +16,7 @@ limitations under the License.
-->
<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
xmlns:g='urn:import:com.google.gwt.user.client.ui'>
<ui:style gss='false' type='com.google.gerrit.client.diff.SkipBar.SkipBarStyle'>
<ui:style gss='false' type='com.google.gerrit.client.diff.UnifiedSkipBar.SkipBarStyle'>
.skipBar {
background-color: #def;
height: 1.3em;

View File

@@ -0,0 +1,78 @@
// 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.patches.SkippedLine;
import net.codemirror.lib.CodeMirror;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/** Collapses common regions with {@link UnifiedSkipBar} for {@link Unified}. */
class UnifiedSkipManager extends SkipManager {
private Unified host;
UnifiedSkipManager(Unified host, UnifiedCommentManager commentManager) {
super(commentManager);
this.host = host;
}
@Override
void render(int context, DiffInfo diff) {
List<SkippedLine> skips = getSkippedLines(context, diff);
if (!skips.isEmpty()) {
CodeMirror cm = host.getCm();
Set<SkipBar> skipBars = new HashSet<>();
setSkipBars(skipBars);
for (SkippedLine skip : skips) {
UnifiedSkipBar bar = newSkipBar(cm, skip);
skipBars.add(bar);
if (skip.getStartA() == 0 || skip.getStartB() == 0) {
bar.upArrow.setVisible(false);
setLine0(bar);
} else if (skip.getStartA() + skip.getSize() == getLineA()
|| skip.getStartB() + skip.getSize() == getLineB()) {
bar.downArrow.setVisible(false);
}
}
}
}
void remove(UnifiedSkipBar bar) {
Set<SkipBar> skipBars = getSkipBars();
skipBars.remove(bar);
if (getLine0() == bar) {
setLine0(null);
}
if (skipBars.isEmpty()) {
setSkipBars(null);
}
}
private UnifiedSkipBar newSkipBar(CodeMirror cm, SkippedLine skip) {
int start = host.getCmLine(skip.getStartA(), DisplaySide.A);
int end = start + skip.getSize() - 1;
UnifiedSkipBar bar = new UnifiedSkipBar(this, cm);
host.getDiffTable().add(bar);
bar.collapse(start, end, true);
return bar;
}
}

View File

@@ -0,0 +1,68 @@
// 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.reviewdb.client.PatchSet;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.HTMLPanel;
/**
* A table with one row and one column to hold a unified CodeMirror displaying
* the files to be compared.
*/
class UnifiedTable extends DiffTable {
interface Binder extends UiBinder<HTMLPanel, UnifiedTable> {}
private static final Binder uiBinder = GWT.create(Binder.class);
interface DiffTableStyle extends CssResource {
String intralineInsert();
String intralineDelete();
String diffInsert();
String diffDelete();
String unifiedLineNumber();
String lineNumbersLeft();
String lineNumbersRight();
}
private Unified parent;
@UiField Element cm;
@UiField static DiffTableStyle style;
UnifiedTable(Unified parent, PatchSet.Id base, PatchSet.Id revision,
String path) {
super(parent, base, revision, path);
initWidget(uiBinder.createAndBindUi(this));
this.parent = parent;
}
@Override
void setHideEmptyPane(boolean hide) {
}
@Override
boolean isVisibleA() {
return true;
}
@Override
Unified getDiffScreen() {
return parent;
}
}

View File

@@ -0,0 +1,140 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
xmlns:g='urn:import:com.google.gwt.user.client.ui'
xmlns:d='urn:import:com.google.gerrit.client.diff'>
<ui:with field='res' type='com.google.gerrit.client.diff.Resources'/>
<ui:style gss='false' type='com.google.gerrit.client.diff.UnifiedTable.DiffTableStyle'>
@external .CodeMirror, .CodeMirror-selectedtext;
@external .CodeMirror-vscrollbar .CodeMirror-scroll;
@external .CodeMirror-dialog-bottom;
@external .CodeMirror-cursor;
@external .dark, .unifiedLineNumber, .noIntraline;
.difftable .patchSetNav,
.difftable .CodeMirror {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.difftable .CodeMirror pre {
overflow: hidden;
border-right: 0;
width: auto;
}
/* Preserve space for underscores. If this changes
* see ChunkManager.addPadding() and adjust there.
*/
.difftable .CodeMirror pre,
.difftable .CodeMirror pre span {
padding-bottom: 1px;
}
.table {
width: 100%;
table-layout: fixed;
border-spacing: 0;
}
.table td { padding: 0 }
/* Hide scrollbars. */
.difftable .CodeMirror-scroll { padding-right: 0; }
.difftable .CodeMirror-vscrollbar { display: none !important; } */
.diffDelete { background-color: #faa; }
.diffInsert { background-color: #9f9; }
.intralineDelete { background-color: #fee; }
.intralineInsert { background-color: #dfd; }
.noIntraline .intralineDelete { background-color: #faa; }
.noIntraline .intralineInsert { background-color: #9f9; }
.dark .diffDelete { background-color: #400; }
.dark .diffInsert { background-color: #444; }
.dark .intralineDelete { background-color: #888; }
.dark .intralineInsert { background-color: #bbb; }
.dark .noIntraline .intralineDelete { background-color: #400; }
.dark .noIntraline .intralineInsert { background-color: #444; }
.patchSetNav, .diff_header {
background-color: #f7f7f7;
line-height: 1;
}
.difftable .CodeMirror-selectedtext {
background-color: inherit !important;
}
.difftable .CodeMirror div.CodeMirror-cursor {
border-left: 2px solid black;
}
.difftable .CodeMirror-dialog-bottom {
border-top: 0;
border-left: 1px solid #000;
border-bottom: 1px solid #000;
background-color: #f7f7f7;
top: 0;
right: 0;
bottom: auto;
left: auto;
}
.lineNumbersLeft, .lineNumbersRight {
min-width: 20px;
width: 3em; /* TODO: This needs to be set based on number of lines */
}
.lineNumbersLeft {
border-right: 1px solid #ddd;
}
.unifiedLineNumber {
cursor: pointer;
padding: 0 3px 0 5px;
min-width: 20px;
text-align: right;
color: #999;
}
</ui:style>
<g:HTMLPanel styleName='{style.difftable}'>
<table class='{style.table}'>
<tr ui:field='patchSetNavRow' class='{style.patchSetNav}'>
<td>
<table class='{style.table}'>
<tr>
<td ui:field='patchSetNavCellA'>
<d:PatchSetSelectBox ui:field='patchSetSelectBoxA' />
</td>
</tr>
<tr>
<td ui:field='patchSetNavCellB'>
<d:PatchSetSelectBox ui:field='patchSetSelectBoxB' />
</td>
</tr>
</table>
</td>
</tr>
<tr ui:field='diffHeaderRow' class='{res.diffTableStyle.diffHeader}'>
<td><pre ui:field='diffHeaderText' /></td>
</tr>
<tr>
<td ui:field='cm'/>
</tr>
</table>
<g:FlowPanel ui:field='widgets' visible='false'/>
</g:HTMLPanel>
</ui:UiBinder>

View File

@@ -376,6 +376,14 @@ public class CodeMirror extends JavaScriptObject {
return Extras.get(this);
}
public final native LineHandle setGutterMarker(int line, String gutterId, Element value) /*-{
return this.setGutterMarker(line, gutterId, value);
}-*/;
public final native LineHandle setGutterMarker(LineHandle line, String gutterId, Element value) /*-{
return this.setGutterMarker(line, gutterId, value);
}-*/;
protected CodeMirror() {
}

View File

@@ -22,6 +22,7 @@
@external .cm-tab;
@external .cm-searching;
@external .cm-trailingspace;
@external .unifiedLineNumber;
/* Reduce margins around CodeMirror to save space. */
.CodeMirror-lines {
@@ -61,7 +62,8 @@
}
/* Highlight current line number in the line gutter. */
.activeLine .CodeMirror-linenumber {
.activeLine .CodeMirror-linenumber,
.activeLine .unifiedLineNumber {
background-color: #bcf !important;
color: #000;
}