blob: 587dacc130d6827e0456dc1cf0879abc2fbd3f84 [file] [log] [blame]
// 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.DiffObject;
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;
import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.ui.CommentLinkProcessor;
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.JsArray;
import java.util.ArrayList;
import java.util.Collection;
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;
import net.codemirror.lib.CodeMirror;
import net.codemirror.lib.Pos;
import net.codemirror.lib.TextMarker.FromTo;
/** Tracks comment widgets for {@link DiffScreen}. */
abstract class CommentManager {
private final DiffObject base;
private final PatchSet.Id revision;
private final String path;
private final CommentLinkProcessor commentLinkProcessor;
final SortedMap<Integer, CommentGroup> sideA;
final SortedMap<Integer, CommentGroup> sideB;
private final Map<String, PublishedBox> published;
private final Set<DraftBox> unsavedDrafts;
final DiffScreen host;
private boolean attached;
private boolean expandAll;
private boolean open;
CommentManager(
DiffScreen host,
DiffObject base,
PatchSet.Id revision,
String path,
CommentLinkProcessor clp,
boolean open) {
this.host = host;
this.base = base;
this.revision = revision;
this.path = path;
this.commentLinkProcessor = clp;
this.open = open;
published = new HashMap<>();
unsavedDrafts = new HashSet<>();
sideA = new TreeMap<>();
sideB = new TreeMap<>();
}
void setAttached(boolean attached) {
this.attached = attached;
}
boolean isAttached() {
return attached;
}
void setExpandAll(boolean expandAll) {
this.expandAll = expandAll;
}
boolean isExpandAll() {
return expandAll;
}
boolean isOpen() {
return open;
}
String getPath() {
return path;
}
Map<String, PublishedBox> getPublished() {
return published;
}
CommentLinkProcessor getCommentLinkProcessor() {
return commentLinkProcessor;
}
void renderDrafts(DisplaySide forSide, JsArray<CommentInfo> in) {
for (CommentInfo info : Natives.asList(in)) {
DisplaySide side = displaySide(info, forSide);
if (side != null) {
addDraftBox(side, info);
}
}
}
void setUnsaved(DraftBox box, boolean isUnsaved) {
if (isUnsaved) {
unsavedDrafts.add(box);
} else {
unsavedDrafts.remove(box);
}
}
void saveAllDrafts(CallbackGroup cb) {
for (DraftBox box : unsavedDrafts) {
box.save(cb);
}
}
Side getStoredSideFromDisplaySide(DisplaySide side) {
if (side == DisplaySide.A && (base.isBaseOrAutoMerge() || base.isParent())) {
return Side.PARENT;
}
return Side.REVISION;
}
int getParentNumFromDisplaySide(DisplaySide side) {
if (side == DisplaySide.A) {
return base.getParentNum();
}
return 0;
}
PatchSet.Id getPatchSetIdFromSide(DisplaySide side) {
if (side == DisplaySide.A && (base.isPatchSet() || base.isEdit())) {
return base.asPatchSetId();
}
return revision;
}
DisplaySide displaySide(CommentInfo info, DisplaySide forSide) {
if (info.side() == Side.PARENT) {
return (base.isBaseOrAutoMerge() || base.isParent()) ? DisplaySide.A : null;
}
return forSide;
}
static FromTo adjustSelection(CodeMirror cm) {
FromTo fromTo = cm.getSelectedRange();
Pos to = fromTo.to();
if (to.ch() == 0) {
to.line(to.line() - 1);
to.ch(cm.getLine(to.line()).length());
}
return fromTo;
}
abstract CommentGroup group(DisplaySide side, int cmLinePlusOne);
/**
* 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.skipManager.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(
getPath(),
getStoredSideFromDisplaySide(side),
getParentNumFromDisplaySide(side),
line,
null,
false))
.setEdit(true);
}
}
abstract String getTokenSuffixForActiveLine(CodeMirror cm);
Runnable signInCallback(final CodeMirror cm) {
return new Runnable() {
@Override
public void run() {
String token = host.getToken();
if (cm.extras().hasActiveLine()) {
token += "@" + getTokenSuffixForActiveLine(cm);
}
Gerrit.doSignIn(token);
}
};
}
abstract void newDraft(CodeMirror cm);
Runnable newDraftCallback(final CodeMirror cm) {
if (!Gerrit.isSignedIn()) {
return signInCallback(cm);
}
return new Runnable() {
@Override
public void run() {
if (cm.extras().hasActiveLine()) {
newDraft(cm);
}
}
};
}
DraftBox addDraftBox(DisplaySide side, CommentInfo info) {
int cmLinePlusOne = host.getCmLine(info.line() - 1, side) + 1;
CommentGroup 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(
host.getDiffTable()
.scrollbar
.draft(host.getCmFromSide(side), Math.max(0, cmLinePlusOne - 1)));
return box;
}
void setExpandAllComments(boolean b) {
setExpandAll(b);
for (CommentGroup g : sideA.values()) {
g.setOpenAll(b);
}
for (CommentGroup g : sideB.values()) {
g.setOpenAll(b);
}
}
abstract SortedMap<Integer, CommentGroup> getMapForNav(DisplaySide side);
Runnable commentNav(final CodeMirror src, final Direction dir) {
return new Runnable() {
@Override
public void run() {
// Every comment appears in both side maps as a linked pair.
// It is only necessary to search one side to find a comment
// on either side of the editor pair.
SortedMap<Integer, CommentGroup> map = getMapForNav(src.side());
int line =
src.extras().hasActiveLine() ? src.getLineNumber(src.extras().activeLine()) + 1 : 0;
CommentGroup g;
if (dir == Direction.NEXT) {
map = map.tailMap(line + 1);
if (map.isEmpty()) {
return;
}
g = map.get(map.firstKey());
while (g.getBoxCount() == 0) {
map = map.tailMap(map.firstKey() + 1);
if (map.isEmpty()) {
return;
}
g = map.get(map.firstKey());
}
} else {
map = map.headMap(line);
if (map.isEmpty()) {
return;
}
g = map.get(map.lastKey());
while (g.getBoxCount() == 0) {
map = map.headMap(map.lastKey());
if (map.isEmpty()) {
return;
}
g = map.get(map.lastKey());
}
}
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 clearLine(DisplaySide side, int line, CommentGroup group) {
SortedMap<Integer, CommentGroup> map = map(side);
if (map.get(line) == group) {
map.remove(line);
}
}
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);
}
void renderPublished(DisplaySide forSide, JsArray<CommentInfo> in) {
for (CommentInfo info : Natives.asList(in)) {
DisplaySide side = displaySide(info, forSide);
if (side != null) {
int cmLinePlusOne = host.getCmLine(info.line() - 1, side) + 1;
CommentGroup group = group(side, cmLinePlusOne);
PublishedBox box =
new PublishedBox(
group,
getCommentLinkProcessor(),
getPatchSetIdFromSide(side),
info,
side,
isOpen());
group.add(box);
box.setAnnotation(
host.getDiffTable().scrollbar.comment(host.getCmFromSide(side), cmLinePlusOne - 1));
getPublished().put(info.id(), box);
}
}
}
abstract Collection<Integer> getLinesWithCommentGroups();
private static void checkAndAddSkip(List<SkippedLine> out, SkippedLine s) {
if (s.getSize() > 1) {
out.add(s);
}
}
List<SkippedLine> splitSkips(int context, List<SkippedLine> skips) {
if (sideA.containsKey(0) || sideB.containsKey(0)) {
// Special case of file comment; cannot skip first line.
for (SkippedLine skip : skips) {
if (skip.getStartA() == 0) {
skip.incrementStart(1);
break;
}
}
}
for (int boxLine : getLinesWithCommentGroups()) {
List<SkippedLine> temp = new ArrayList<>(skips.size() + 2);
for (SkippedLine skip : skips) {
int startLine = host.getCmLine(skip.getStartB(), DisplaySide.B);
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;
}
abstract void newDraftOnGutterClick(CodeMirror cm, String gutterClass, int line);
abstract CommentGroup getCommentGroupOnActiveLine(CodeMirror cm);
Runnable toggleOpenBox(final CodeMirror cm) {
return new Runnable() {
@Override
public void run() {
CommentGroup group = getCommentGroupOnActiveLine(cm);
if (group != null) {
group.openCloseLast();
}
}
};
}
Runnable openCloseAll(final CodeMirror cm) {
return new Runnable() {
@Override
public void run() {
CommentGroup group = getCommentGroupOnActiveLine(cm);
if (group != null) {
group.openCloseAll();
}
}
};
}
SortedMap<Integer, CommentGroup> map(DisplaySide side) {
return side == DisplaySide.A ? sideA : sideB;
}
}