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