// 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(CodeMirror cm) { return () -> { String token = host.getToken(); if (cm.extras().hasActiveLine()) { token += "@" + getTokenSuffixForActiveLine(cm); } Gerrit.doSignIn(token); }; } abstract void newDraft(CodeMirror cm); Runnable newDraftCallback(CodeMirror cm) { if (!Gerrit.isSignedIn()) { return signInCallback(cm); } return () -> { 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(CodeMirror src, Direction dir) { return () -> { // 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(CodeMirror cm) { return () -> { CommentGroup group = getCommentGroupOnActiveLine(cm); if (group != null) { group.openCloseLast(); } }; } Runnable openCloseAll(CodeMirror cm) { return () -> { CommentGroup group = getCommentGroupOnActiveLine(cm); if (group != null) { group.openCloseAll(); } }; } SortedMap<Integer, CommentGroup> map(DisplaySide side) { return side == DisplaySide.A ? sideA : sideB; } }