From f156a4d76a1521d9aeb8d029a5703e44cbef7b55 Mon Sep 17 00:00:00 2001 From: Michael Zhou Date: Tue, 20 Aug 2013 22:58:39 -0700 Subject: [PATCH] New unified diff based on CodeMirror Migrate unified diff screen from prettify to CodeMirror, reusing as much as possible from the side by side diff view. This now builds and mostly works. TODO: 1. Maybe implement proper behavior for range based comments. - Currently, pressing 'c' with text selected does not open a comment. 2. Support hiding line numbers in Unified. 3. Factor out more common code, minimize duplication. - UnifiedCommentManager and SideBySideCommentManager. - UnifiedSkipBar and SideBySideSkipBar. These will be addressed in follow-up commits. Change-Id: I91935613510879436a911ff1dcc191c729f0f06d --- .../com/google/gerrit/client/Dispatcher.java | 20 +- .../gerrit/client/diff/ChunkManager.java | 306 +----- .../google/gerrit/client/diff/CommentBox.java | 4 +- .../gerrit/client/diff/CommentGroup.java | 127 +-- .../gerrit/client/diff/CommentManager.java | 404 ++------ .../google/gerrit/client/diff/DiffInfo.java | 20 + .../google/gerrit/client/diff/DiffScreen.java | 907 ++++++++++++++++++ .../google/gerrit/client/diff/DiffTable.css | 40 + .../google/gerrit/client/diff/DiffTable.java | 84 +- .../com/google/gerrit/client/diff/Header.java | 11 +- .../client/diff/InsertCommentBubble.java | 2 +- .../gerrit/client/diff/PatchSetSelectBox.java | 14 +- .../gerrit/client/diff/PreferencesAction.java | 4 +- .../gerrit/client/diff/PreferencesBox.java | 29 +- .../gerrit/client/diff/PublishedBox.java | 6 +- .../google/gerrit/client/diff/Resources.java | 1 + .../client/diff/ScrollSynchronizer.java | 4 +- .../google/gerrit/client/diff/SideBySide.java | 868 ++--------------- .../gerrit/client/diff/SideBySide.ui.xml | 2 +- .../client/diff/SideBySideChunkManager.java | 280 ++++++ .../client/diff/SideBySideCommentGroup.java | 128 +++ .../client/diff/SideBySideCommentManager.java | 408 ++++++++ .../gerrit/client/diff/SideBySideSkipBar.java | 217 +++++ .../client/diff/SideBySideSkipBar.ui.xml | 52 + .../client/diff/SideBySideSkipManager.java | 85 ++ .../gerrit/client/diff/SideBySideTable.java | 105 ++ ...iffTable.ui.xml => SideBySideTable.ui.xml} | 30 +- .../google/gerrit/client/diff/SkipBar.java | 186 +--- .../gerrit/client/diff/SkipManager.java | 87 +- .../google/gerrit/client/diff/Unified.java | 427 +++++++++ .../google/gerrit/client/diff/Unified.ui.xml | 30 + .../client/diff/UnifiedChunkManager.java | 316 ++++++ .../client/diff/UnifiedCommentGroup.java | 89 ++ .../client/diff/UnifiedCommentManager.java | 394 ++++++++ .../client/diff/UnifiedDiffChunkInfo.java | 30 + .../gerrit/client/diff/UnifiedSkipBar.java | 193 ++++ .../{SkipBar.ui.xml => UnifiedSkipBar.ui.xml} | 2 +- .../client/diff/UnifiedSkipManager.java | 78 ++ .../gerrit/client/diff/UnifiedTable.java | 68 ++ .../gerrit/client/diff/UnifiedTable.ui.xml | 140 +++ .../java/net/codemirror/lib/CodeMirror.java | 8 + .../main/java/net/codemirror/lib/style.css | 4 +- 42 files changed, 4372 insertions(+), 1838 deletions(-) create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.css create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideChunkManager.java create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentGroup.java create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideSkipBar.java create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideSkipBar.ui.xml create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideSkipManager.java create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java rename gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/{DiffTable.ui.xml => SideBySideTable.ui.xml} (88%) create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.ui.xml create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentGroup.java create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentManager.java create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedDiffChunkInfo.java create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedSkipBar.java rename gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/{SkipBar.ui.xml => UnifiedSkipBar.ui.xml} (93%) create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedSkipManager.java create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.java create mode 100644 gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.ui.xml diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java index 48b217b168..043a54ad6b 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java @@ -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) { diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java index 8ff11e8cdd..e7a2d82f6d 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java @@ -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 chunks; private List markers; private List undo; - private List padding; - private List 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 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 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 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 getDiffChunkComparator() { + Comparator 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); +} \ No newline at end of file diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java index 0e85a2f689..4654a3d8b0 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentBox.java @@ -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; diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentGroup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentGroup.java index a8a56fcc24..d48ada7497 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentGroup.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentGroup.java @@ -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(); } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java index 4e1a3e1e8b..43864361ae 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java @@ -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 published; - private final SortedMap sideA; - private final SortedMap sideB; private final Set 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 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 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 in) { + String getPath() { + return path; + } + + Map getPublished() { + return published; + } + + CommentLinkProcessor getCommentLinkProcessor() { + return commentLinkProcessor; + } + + void renderDrafts(DisplaySide forSide, JsArray 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 splitSkips(int context, List 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 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 out, SkippedLine s) { - if (s.getSize() > 1) { - out.add(s); - } - } - - void clearLine(DisplaySide side, int line, CommentGroup group) { - SortedMap 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 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 in); + + abstract List splitSkips(int context, List 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(); } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java index 97453faaf8..82dad3f908 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java @@ -118,6 +118,26 @@ public class DiffInfo extends JavaScriptObject { return s.toString(); } + public final String textUnified() { + StringBuilder s = new StringBuilder(); + JsArray 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'); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java new file mode 100644 index 0000000000..04d91f7efb --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java @@ -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 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() { + final AsyncCallback 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() { + final AsyncCallback 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() { + @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() { + @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 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(""); + } + }) + .on("Shift-Space", new Runnable() { + @Override + public void run() { + cm.vim().handleKey(""); + } + }) + .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 + 3 AuthorDate: 2015-02-27 19:20:52 +0900 + 4 Commit: A. U. Thor + 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() { + @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 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() { + @Override + public void onSuccess(DiffInfo info) { + new ModeInjector() + .add(getContentType(info.metaA())) + .add(getContentType(info.metaB())) + .inject(CallbackGroup. 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() { + @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 getScreenLoadCallback( + CommentsCollections comments); +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.css new file mode 100644 index 0000000000..2378f18e26 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.css @@ -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 {} \ No newline at end of file diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java index 4b073f55b7..e4407cc21c 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.java @@ -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 {} - 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(); } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java index 3d7607bed8..b538f85dc1 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java @@ -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) { diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.java index 7c8bc21885..ac295e389f 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.java @@ -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()); } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java index 186cd98f88..8547d1a25b 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java @@ -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() { diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesAction.java index 869b4a300c..3f7a0abb08 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesAction.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesAction.java @@ -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; } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java index f363f9bcc8..0cdfe5df59 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java @@ -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 e) { - view.diffTable.setVisibleA(e.getValue()); + if (view.getDiffTable() instanceof SideBySideTable) { + ((SideBySideTable) view.getDiffTable()).setVisibleA(e.getValue()); + } } @UiHandler("emptyPane") void onHideEmptyPane(ValueChangeEvent 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); + } } }); } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java index 9e9c93a5ec..5a9219a6bf 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PublishedBox.java @@ -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); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java index 10b5af026c..0359df3e7a 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Resources.java @@ -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): diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollSynchronizer.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollSynchronizer.java index 4ee09d25d8..13f58db45c 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollSynchronizer.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollSynchronizer.java @@ -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; diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java index 33a2fb8098..94beb0972e 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java @@ -14,50 +14,26 @@ 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.InlineHyperlink; -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.GWT; -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.FocusEvent; 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.UiBinder; import com.google.gwt.uibinder.client.UiField; import com.google.gwt.user.client.Window; @@ -66,77 +42,31 @@ import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.ImageResourceRenderer; 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.Configuration; 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.Collections; -import java.util.EnumSet; import java.util.List; -public class SideBySide extends Screen { - private static final KeyMap RENDER_ENTIRE_FILE_KEYMAP = KeyMap.create() - .propagate("Ctrl-F"); - +public class SideBySide extends DiffScreen { interface Binder extends UiBinder {} private static final Binder uiBinder = GWT.create(Binder.class); - enum FileSize { - SMALL(0), - LARGE(500), - HUGE(4000); - - final int lines; - - FileSize(int n) { - this.lines = n; - } - } - @UiField(provided = true) - Header header; - - @UiField(provided = true) - DiffTable diffTable; - - 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; + SideBySideTable diffTable; private CodeMirror cmA; private CodeMirror cmB; - private HandlerRegistration resizeHandler; private ScrollSynchronizer scrollSynchronizer; - private DiffInfo diff; - private FileSize fileSize; - private EditInfo edit; - private ChunkManager chunkManager; - private CommentManager commentManager; - private SkipManager skipManager; - private KeyCommandSet keysNavigation; - private KeyCommandSet keysAction; - private KeyCommandSet keysComment; - private List handlers; - private PreferencesAction prefsAction; - private int reloadVersionId; + private SideBySideChunkManager chunkManager; + private SideBySideCommentManager commentManager; + private SideBySideSkipManager skipManager; public SideBySide( PatchSet.Id base, @@ -144,149 +74,33 @@ public class SideBySide extends Screen { String path, DisplaySide startSide, int startLine) { - this.base = base; - this.revision = revision; - this.changeId = revision.getParentKey(); - this.path = path; - this.startSide = startSide; - this.startLine = startLine; + super(base, revision, path, startSide, startLine, DiffScreenType.SIDE_BY_SIDE); - prefs = DiffPreferences.create(Gerrit.getDiffPreferences()); - handlers = new ArrayList<>(6); - keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation()); - header = new Header(keysNavigation, base, revision, path); - diffTable = new DiffTable(this, base, revision, path); + diffTable = new SideBySideTable(this, base, revision, path); add(uiBinder.createAndBindUi(this)); addDomHandler(GlobalKey.STOP_PROPAGATION, KeyPressEvent.getType()); } @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() { - final AsyncCallback themeCallback = group2.addEmpty(); - + ScreenLoadCallback getScreenLoadCallback( + final CommentsCollections comments) { + return new ScreenLoadCallback(SideBySide.this) { @Override - public void onSuccess(Void result) { - // Load theme after CM library to ensure theme can override CSS. - ThemeLoader.loadTheme(prefs.theme(), themeCallback); + protected void preDisplay(ConfigInfoCache.Entry result) { + commentManager = new SideBySideCommentManager( + SideBySide.this, + getBase(), getRevision(), getPath(), + result.getCommentLinkProcessor(), + getChangeStatus().isOpen()); + setTheme(result.getTheme()); + display(comments); } - - @Override - public void onFailure(Throwable caught) { - } - })); - - DiffApi.diff(revision, path) - .base(base) - .wholeFile() - .intraline(prefs.intralineDifference()) - .ignoreWhitespace(prefs.ignoreWhitespace()) - .get(group1.addFinal(new GerritCallback() { - final AsyncCallback 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() { - @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() { - @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 list = info.revisions().values(); - RevisionInfo.sortRevisionInfoByNumber(list); - diffTable.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( - new ScreenLoadCallback(SideBySide.this) { - @Override - protected void preDisplay(ConfigInfoCache.Entry result) { - commentManager = new CommentManager( - SideBySide.this, - base, revision, path, - result.getCommentLinkProcessor(), - changeStatus.isOpen()); - setTheme(result.getTheme()); - display(comments); - } - })); + }; } @Override public void onShowView() { super.onShowView(); - Window.enableScrolling(false); - JumpKeys.enable(false); - if (prefs.hideTopMenu()) { - Gerrit.setHeaderVisible(false); - } - resizeHandler = Window.addResizeHandler(new ResizeHandler() { - @Override - public void onResize(ResizeEvent event) { - resizeCodeMirror(); - } - }); operation(new Runnable() { @Override @@ -297,293 +111,93 @@ public class SideBySide extends Screen { cmB.refresh(); } }); - setLineLength(Patch.COMMIT_MSG.equals(path) ? 72 : prefs.lineLength()); + setLineLength(Patch.COMMIT_MSG.equals(getPath()) ? 72 : getPrefs().lineLength()); diffTable.refresh(); - if (startLine == 0) { + if (getStartLine() == 0) { DiffChunkInfo d = chunkManager.getFirst(); if (d != null) { if (d.isEdit() && d.getSide() == DisplaySide.A) { - startSide = DisplaySide.B; - startLine = lineOnOther(d.getSide(), d.getStart()).getLine() + 1; + setStartSide(DisplaySide.B); + setStartLine(lineOnOther(d.getSide(), d.getStart()).getLine() + 1); } else { - startSide = d.getSide(); - startLine = d.getStart() + 1; + setStartSide(d.getSide()); + setStartLine(d.getStart() + 1); } } } - if (startSide != null && startLine > 0) { - CodeMirror cm = getCmFromSide(startSide); - cm.scrollToLine(startLine - 1); + if (getStartSide() != null && getStartLine() > 0) { + CodeMirror cm = getCmFromSide(getStartSide()); + cm.scrollToLine(getStartLine() - 1); cm.focus(); } else { cmA.setCursor(Pos.create(0)); cmA.focus(); } - if (Gerrit.isSignedIn() && prefs.autoReview()) { + if (Gerrit.isSignedIn() && getPrefs().autoReview()) { header.autoReview(); } prefetchNextFile(); } @Override - protected void onUnload() { - super.onUnload(); + void registerCmEvents(final CodeMirror cm) { + super.registerCmEvents(cm); - removeKeyHandlerRegistrations(); - if (commentManager != null) { - CallbackGroup group = new CallbackGroup(); - commentManager.saveAllDrafts(group); - group.done(); - } - if (resizeHandler != null) { - resizeHandler.removeHandler(); - resizeHandler = null; - } - if (cmA != null) { - cmA.getWrapperElement().removeFromParent(); - } - if (cmB != null) { - cmB.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(); - } - - private 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", commentManager.toggleOpenBox(cm)) - .on("Enter", commentManager.toggleOpenBox(cm)) - .on("N", maybeNextVimSearch(cm)) - .on("E", openEditScreen(cm)) - .on("P", chunkManager.diffChunkNav(cm, Direction.PREV)) .on("Shift-A", diffTable.toggleA()) - .on("Shift-M", header.reviewedAndNext()) - .on("Shift-N", maybePrevVimSearch(cm)) - .on("Shift-P", commentManager.commentNav(cm, Direction.PREV)) - .on("Shift-O", commentManager.openCloseAll(cm)) .on("Shift-Left", moveCursorToSide(cm, DisplaySide.A)) - .on("Shift-Right", moveCursorToSide(cm, DisplaySide.B)) - .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(""); - } - }) - .on("Shift-Space", new Runnable() { - @Override - public void run() { - cm.vim().handleKey(""); - } - }) - .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", commentManager.insertNewDraft(cm)); - } + .on("Shift-Right", moveCursorToSide(cm, DisplaySide.B)); cm.addKeyMap(keyMap); - if (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(commentManager, cm); - add(bubble); - cm.addWidget(anchor, bubble.getElement()); - } - }; + maybeRegisterRenderEntireFileKeyMap(cm); } @Override public void registerKeys() { super.registerKeys(); - keysNavigation.add(new UpToChangeCommand(revision, 0, 'u')); - keysNavigation.add( + getKeysNavigation().add( new NoOpKeyCommand(KeyCommand.M_SHIFT, KeyCodes.KEY_LEFT, PatchUtil.C.focusSideA()), new NoOpKeyCommand(KeyCommand.M_SHIFT, KeyCodes.KEY_RIGHT, PatchUtil.C.focusSideB())); - 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, 'e', PatchUtil.C.openEditScreen())); - 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( + getKeysAction().add(new KeyCommand( KeyCommand.M_SHIFT, 'a', PatchUtil.C.toggleSideA()) { @Override public void onKeyPress(KeyPressEvent event) { diffTable.toggleA().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; - } + 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(new FocusHandler() { + @Override + FocusHandler getFocusHandler() { + return new FocusHandler() { @Override public void onFocus(FocusEvent event) { cmB.focus(); } - })); + }; } private void display(final CommentsCollections comments) { + final DiffPreferences prefs = getPrefs(); + final DiffInfo diff = getDiff(); setThemeStyles(prefs.theme().isDark()); setShowIntraline(prefs.intralineDifference()); if (prefs.showLineNumbers()) { - diffTable.addStyleName(DiffTable.style.showLineNumbers()); + diffTable.addStyleName(SideBySideTable.style.showLineNumbers()); } - cmA = newCM(diff.metaA(), diff.textA(), diffTable.cmA); - cmB = newCM(diff.metaB(), diff.textB(), diffTable.cmB); + cmA = newCm(diff.metaA(), diff.textA(), diffTable.cmA); + cmB = newCm(diff.metaB(), diff.textB(), diffTable.cmB); cmA.extras().side(DisplaySide.A); cmB.extras().side(DisplaySide.B); setShowTabs(prefs.showTabs()); - chunkManager = new ChunkManager(this, cmA, cmB, diffTable.scrollbar); - skipManager = new SkipManager(this, commentManager); + chunkManager = new SideBySideChunkManager(this, cmA, cmB, diffTable.scrollbar); + skipManager = new SideBySideSkipManager(this, commentManager); operation(new Runnable() { @Override @@ -605,37 +219,30 @@ public class SideBySide extends Screen { scrollSynchronizer = new ScrollSynchronizer(diffTable, cmA, cmB, chunkManager.getLineMapper()); - prefsAction = new PreferencesAction(this, prefs); - header.init(prefsAction, getLinks(), diff.sideBySideWebLinks()); + setPrefsAction(new PreferencesAction(this, prefs)); + header.init(getPrefsAction(), getUnifiedDiffLink(), diff.sideBySideWebLinks()); scrollSynchronizer.setAutoHideDiffTableHeader(prefs.autoHideDiffTableHeader()); - 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); - } + setupSyntaxHighlighting(); } - private List getLinks() { + private List getUnifiedDiffLink() { InlineHyperlink toUnifiedDiffLink = new InlineHyperlink(); toUnifiedDiffLink.setHTML( new ImageResourceRenderer().render(Gerrit.RESOURCES.unifiedDiff())); toUnifiedDiffLink.setTargetHistoryToken( - Dispatcher.toUnified(base, revision, path)); + Dispatcher.toUnified(getBase(), getRevision(), getPath())); toUnifiedDiffLink.setTitle(PatchUtil.C.unifiedDiff()); return Collections.singletonList(toUnifiedDiffLink); } - private CodeMirror newCM( + @Override + CodeMirror newCm( DiffInfo.FileMeta meta, String contents, Element parent) { + DiffPreferences prefs = getPrefs(); + return CodeMirror.create(parent, Configuration.create() .set("cursorBlinkRate", prefs.cursorBlinkRate()) .set("cursorHeight", 0.85) @@ -643,7 +250,7 @@ public class SideBySide extends Screen { .set("lineNumbers", prefs.showLineNumbers()) .set("lineWrapping", false) .set("matchBrackets", prefs.matchBrackets()) - .set("mode", fileSize == FileSize.SMALL ? getContentType(meta) : null) + .set("mode", getFileSize() == FileSize.SMALL ? getContentType(meta) : null) .set("readOnly", true) .set("scrollbarStyle", "overlay") .set("showTrailingSpace", prefs.showWhitespaceErrors()) @@ -654,69 +261,21 @@ public class SideBySide extends Screen { .set("viewportMargin", renderEntireFile() ? POSITIVE_INFINITY : 10)); } - DiffInfo.IntraLineStatus getIntraLineStatus() { - return diff.intralineStatus(); - } - - 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); - } - - String getContentType() { - return getContentType(diff.metaB()); - } - - void setThemeStyles(boolean d) { - if (d) { - diffTable.addStyleName(DiffTable.style.dark()); - } else { - diffTable.removeStyleName(DiffTable.style.dark()); - } - } - - void setShowTabs(boolean show) { - cmA.extras().showTabs(show); - cmB.extras().showTabs(show); - } - - void setLineLength(int columns) { - cmA.extras().lineLength(columns); - cmB.extras().lineLength(columns); - } - + @Override void setShowLineNumbers(boolean b) { cmA.setOption("lineNumbers", b); cmB.setOption("lineNumbers", b); if (b) { - diffTable.addStyleName(DiffTable.style.showLineNumbers()); + diffTable.addStyleName(SideBySideTable.style.showLineNumbers()); } else { - diffTable.removeStyleName(DiffTable.style.showLineNumbers()); + diffTable.removeStyleName(SideBySideTable.style.showLineNumbers()); } } - void setShowIntraline(boolean b) { - if (b && getIntraLineStatus() == DiffInfo.IntraLineStatus.OFF) { - reloadDiffInfo(); - } else if (b) { - diffTable.removeStyleName(DiffTable.style.noIntraline()); - } else { - diffTable.addStyleName(DiffTable.style.noIntraline()); - } - } - - private void toggleShowIntraline() { - prefs.intralineDifference(!prefs.intralineDifference()); - setShowIntraline(prefs.intralineDifference()); - prefsAction.update(); - } - + @Override void setSyntaxHighlighting(boolean b) { + final DiffInfo diff = getDiff(); + final DiffPreferences prefs = getPrefs(); if (b) { injectMode(diff, new AsyncCallback() { @Override @@ -738,39 +297,22 @@ public class SideBySide extends Screen { } } - void setContext(final int context) { - operation(new Runnable() { - @Override - public void run() { - skipManager.removeAll(); - skipManager.render(context, diff); - updateRenderEntireFile(); - } - }); - } - + @Override void setAutoHideDiffHeader(boolean hide) { scrollSynchronizer.setAutoHideDiffTableHeader(hide); } - private void render(DiffInfo diff) { - header.setNoDiff(diff); - chunkManager.render(diff); - } - CodeMirror otherCm(CodeMirror me) { return me == cmA ? cmB : cmA; } + @Override CodeMirror getCmFromSide(DisplaySide side) { return side == DisplaySide.A ? cmA : cmB; } - LineOnOtherInfo lineOnOther(DisplaySide side, int line) { - return chunkManager.getLineMapper().lineOnOther(side, line); - } - - private Runnable updateActiveLine(final CodeMirror cm) { + @Override + Runnable updateActiveLine(final CodeMirror cm) { final CodeMirror other = otherCm(cm); return new Runnable() { @Override @@ -807,50 +349,6 @@ public class SideBySide extends Screen { }; } - private GutterClickHandler onGutterClick(final CodeMirror cm) { - return new GutterClickHandler() { - @Override - public void handle(CodeMirror instance, final int line, String gutter, - 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() { - commentManager.newDraft(cm, line + 1); - } - }); - } - } - }; - } - - private Runnable upToChange(final boolean openReplyBox) { - return new Runnable() { - @Override - public void run() { - CallbackGroup group = new CallbackGroup(); - commentManager.saveAllDrafts(group); - group.done(); - group.addListener(new GerritCallback() { - @Override - public void onSuccess(Void result) { - String b = base != null ? base.getId() : null; - String rev = revision.getId(); - Gerrit.display( - PageLinks.toChange(changeId, b, rev), - new ChangeScreen(changeId, b, rev, openReplyBox, - FileTable.Mode.REVIEW)); - } - }); - } - }; - } - private Runnable moveCursorToSide(final CodeMirror cmSrc, DisplaySide sideDst) { final CodeMirror cmDst = getCmFromSide(sideDst); if (cmDst == cmSrc) { @@ -875,156 +373,13 @@ public class SideBySide extends Screen { }; } - private Runnable maybePrevVimSearch(final CodeMirror cm) { - return new Runnable() { - @Override - public void run() { - if (cm.vim().hasSearchHighlight()) { - cm.vim().handleKey("N"); - } else { - commentManager.commentNav(cm, Direction.NEXT).run(); - } - } - }; - } - - private Runnable maybeNextVimSearch(final CodeMirror cm) { - return new Runnable() { - @Override - public void run() { - if (cm.vim().hasSearchHighlight()) { - cm.vim().handleKey("n"); - } else { - chunkManager.diffChunkNav(cm, Direction.NEXT).run(); - } - } - }; - } - - private int adjustCommitMessageLine(int line) { - /* When commit messages are shown in the side-by-side screen they include - a header block that looks like this: - - 1 Parent: deadbeef (Parent commit title) - 2 Author: A. U. Thor - 3 AuthorDate: 2015-02-27 19:20:52 +0900 - 4 Commit: A. U. Thor - 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() { - cmA.removeKeyMap(RENDER_ENTIRE_FILE_KEYMAP); - cmB.removeKeyMap(RENDER_ENTIRE_FILE_KEYMAP); - - boolean entireFile = renderEntireFile(); - if (entireFile) { - cmA.addKeyMap(RENDER_ENTIRE_FILE_KEYMAP); - cmB.addKeyMap(RENDER_ENTIRE_FILE_KEYMAP); - } - cmA.setOption("viewportMargin", entireFile ? POSITIVE_INFINITY : 10); - cmB.setOption("viewportMargin", entireFile ? POSITIVE_INFINITY : 10); - } - - void resizeCodeMirror() { - int hdr = header.getOffsetHeight() + diffTable.getHeaderHeight(); - cmA.adjustHeight(hdr); - cmB.adjustHeight(hdr); - } - void syncScroll(DisplaySide masterSide) { if (scrollSynchronizer != null) { scrollSynchronizer.syncScroll(masterSide); } } - private 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; - } - - private void injectMode(DiffInfo diffInfo, AsyncCallback cb) { - new ModeInjector() - .add(getContentType(diffInfo.metaA())) - .add(getContentType(diffInfo.metaB())) - .inject(cb); - } - - String getPath() { - return path; - } - - DiffPreferences getPrefs() { - return prefs; - } - - ChunkManager getChunkManager() { - return chunkManager; - } - - CommentManager getCommentManager() { - return commentManager; - } - - SkipManager getSkipManager() { - return skipManager; - } - + @Override void operation(final Runnable apply) { cmA.operation(new Runnable() { @Override @@ -1039,70 +394,33 @@ public class SideBySide extends Screen { }); } - private 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() { - @Override - public void onSuccess(DiffInfo info) { - new ModeInjector() - .add(getContentType(info.metaA())) - .add(getContentType(info.metaB())) - .inject(CallbackGroup. emptyCallback()); - } - - @Override - public void onFailure(Throwable caught) { - } - }); - } + @Override + int getCodeMirrorHeight() { + return header.getOffsetHeight() + diffTable.getHeaderHeight(); } - void reloadDiffInfo() { - final int id = ++reloadVersionId; - DiffApi.diff(revision, path) - .base(base) - .wholeFile() - .intraline(prefs.intralineDifference()) - .ignoreWhitespace(prefs.ignoreWhitespace()) - .get(new GerritCallback() { - @Override - public void onSuccess(DiffInfo diffInfo) { - if (id == reloadVersionId && isAttached()) { - diff = diffInfo; - operation(new Runnable() { - @Override - public void run() { - skipManager.removeAll(); - chunkManager.reset(); - diffTable.scrollbar.removeDiffAnnotations(); - setShowIntraline(prefs.intralineDifference()); - render(diff); - chunkManager.adjustPadding(); - skipManager.render(prefs.context(), diff); - } - }); - } - } - }); + @Override + CodeMirror[] getCms() { + return new CodeMirror[]{cmA, cmB}; } - private 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; + @Override + SideBySideTable getDiffTable() { + return diffTable; + } + + @Override + SideBySideChunkManager getChunkManager() { + return chunkManager; + } + + @Override + SideBySideCommentManager getCommentManager() { + return commentManager; + } + + @Override + SideBySideSkipManager getSkipManager() { + return skipManager; } } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.ui.xml index a4c2eb9714..55c9de0d44 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.ui.xml +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.ui.xml @@ -25,6 +25,6 @@ limitations under the License. - + diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideChunkManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideChunkManager.java new file mode 100644 index 0000000000..20d70389a7 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideChunkManager.java @@ -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 chunks; + private List padding; + private List 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 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; + } +} \ No newline at end of file diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentGroup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentGroup.java new file mode 100644 index 0000000000..216fbdabaf --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentGroup.java @@ -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(); + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java new file mode 100644 index 0000000000..bf955128b1 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentManager.java @@ -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 sideA; + private final SortedMap 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 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 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 splitSkips(int context, List 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 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 out, SkippedLine s) { + if (s.getSize() > 1) { + out.add(s); + } + } + + @Override + void clearLine(DisplaySide side, int line, CommentGroup group) { + SortedMap 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 map(DisplaySide side) { + return side == DisplaySide.A ? sideA : sideB; + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideSkipBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideSkipBar.java new file mode 100644 index 0000000000..5020fed244 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideSkipBar.java @@ -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 {} + 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(); + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideSkipBar.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideSkipBar.ui.xml new file mode 100644 index 0000000000..d51f3d88e4 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideSkipBar.ui.xml @@ -0,0 +1,52 @@ + + + + + .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; + } + + +
+ + + + + +
+
+
\ No newline at end of file diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideSkipManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideSkipManager.java new file mode 100644 index 0000000000..64f10fcbad --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideSkipManager.java @@ -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 skips = getSkippedLines(context, diff); + + if (!skips.isEmpty()) { + CodeMirror cmA = host.getCmFromSide(DisplaySide.A); + CodeMirror cmB = host.getCmFromSide(DisplaySide.B); + + Set 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 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; + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java new file mode 100644 index 0000000000..79865ff16c --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java @@ -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 {} + 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; + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.ui.xml similarity index 88% rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.ui.xml index 99ecc9ba94..63a202b01c 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffTable.ui.xml +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.ui.xml @@ -17,17 +17,15 @@ limitations under the License. - + + @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; - } @@ -152,7 +132,7 @@ limitations under the License. - + diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java index 258eec6a9f..24aedadd12 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java @@ -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 {} - 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); } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java index 5376588e71..8290d98bcd 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java @@ -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 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 getSkippedLines(int context, DiffInfo diff) { if (context == DiffPreferencesInfo.WHOLE_FILE_CONTEXT) { - return; + return new ArrayList<>(); } + lineA = 0; + lineB = 0; JsArray regions = diff.content(); List 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 bars) { + skipBars = bars; + } + + Set getSkipBars() { + return skipBars; } } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java new file mode 100644 index 0000000000..a63fcbc551 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java @@ -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 {} + 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 getScreenLoadCallback( + final CommentsCollections comments) { + return new ScreenLoadCallback(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 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() { + @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; + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.ui.xml new file mode 100644 index 0000000000..85f46a6610 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.ui.xml @@ -0,0 +1,30 @@ + + + + + .unified { + margin-left: -5px; + margin-right: -5px; + } + + + + + + diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java new file mode 100644 index 0000000000..f1634953b5 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java @@ -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 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 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 getDiffChunkComparatorCmLine() { + return new Comparator() { + @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; + } + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentGroup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentGroup.java new file mode 100644 index 0000000000..4effb46dc1 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentGroup.java @@ -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(); + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentManager.java new file mode 100644 index 0000000000..880c8b0ba3 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentManager.java @@ -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 sideA; + private final SortedMap 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 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 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 splitSkips(int context, List 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 allBoxLines = new TreeSet<>(sideA.tailMap(1).keySet()); + allBoxLines.addAll(sideB.tailMap(1).keySet()); + for (int boxLine : allBoxLines) { + List 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 out, SkippedLine s) { + if (s.getSize() > 1) { + out.add(s); + } + } + + @Override + void clearLine(DisplaySide side, int cmLinePlusOne, CommentGroup group) { + SortedMap 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 map(DisplaySide side) { + return side == DisplaySide.A ? sideA : sideB; + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedDiffChunkInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedDiffChunkInfo.java new file mode 100644 index 0000000000..844be78f70 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedDiffChunkInfo.java @@ -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; + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedSkipBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedSkipBar.java new file mode 100644 index 0000000000..4cbf9b0b66 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedSkipBar.java @@ -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 {} + 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(); + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedSkipBar.ui.xml similarity index 93% rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.ui.xml rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedSkipBar.ui.xml index bf3c425c03..ad05ada5f7 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.ui.xml +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedSkipBar.ui.xml @@ -16,7 +16,7 @@ limitations under the License. --> - + .skipBar { background-color: #def; height: 1.3em; diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedSkipManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedSkipManager.java new file mode 100644 index 0000000000..7554a87ce6 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedSkipManager.java @@ -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 skips = getSkippedLines(context, diff); + + if (!skips.isEmpty()) { + CodeMirror cm = host.getCm(); + + Set 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 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; + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.java new file mode 100644 index 0000000000..5d33a200da --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.java @@ -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 {} + 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; + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.ui.xml new file mode 100644 index 0000000000..b9a78eba0b --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedTable.ui.xml @@ -0,0 +1,140 @@ + + + + + + @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; + } + + +
+ + + + + + + + +
+ + + + + + + +
+ +
+ +
+
+
+ +
+
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java index d0c6f9dcd3..9bc9ad44e1 100644 --- a/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java +++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/CodeMirror.java @@ -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() { } diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/style.css b/gerrit-gwtui/src/main/java/net/codemirror/lib/style.css index 4b97da1640..aa4c0021dc 100644 --- a/gerrit-gwtui/src/main/java/net/codemirror/lib/style.css +++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/style.css @@ -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; }