/*
* Copyright 2013 Urs Wolfer
*
* 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.urswolfer.intellij.plugin.gerrit.ui.diff;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.google.common.primitives.Longs;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.Comment;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.inject.Inject;
import com.intellij.codeInsight.highlighting.HighlightManager;
import com.intellij.icons.AllIcons;
import com.intellij.ide.DataManager;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.diff.DiffRequest;
import com.intellij.openapi.diff.impl.DiffPanelImpl;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.markup.HighlighterLayer;
import com.intellij.openapi.editor.markup.MarkupModel;
import com.intellij.openapi.editor.markup.RangeHighlighter;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.AsyncResult;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.FilePathImpl;
import com.intellij.ui.JBColor;
import com.intellij.ui.PopupHandler;
import com.intellij.util.Consumer;
import com.urswolfer.intellij.plugin.gerrit.SelectedRevisions;
import com.urswolfer.intellij.plugin.gerrit.rest.GerritUtil;
import com.urswolfer.intellij.plugin.gerrit.util.GerritDataKeys;
import com.urswolfer.intellij.plugin.gerrit.util.PathUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* @author Urs Wolfer
*
* Some parts based on code from:
* https://github.com/ktisha/Crucible4IDEA
*/
public class CommentsDiffTool extends CustomizableFrameDiffTool {
private static final Predicate<Comment> REVISION_COMMENT = new Predicate<Comment>() {
@Override
public boolean apply(Comment comment) {
return comment.side == null || comment.side.equals(Comment.Side.REVISION);
}
};
private static final Ordering<Comment> COMMENT_ORDERING = new Ordering<Comment>() {
@Override
public int compare(Comment left, Comment right) {
// need to sort descending as icons are added to the left of existing icons
return -Longs.compare(left.updated.getTime(), right.updated.getTime());
}
};
@Inject
private GerritUtil gerritUtil;
@Inject
private DataManager dataManager;
@Inject
private AddCommentActionBuilder addCommentActionBuilder;
@Inject
private PathUtils pathUtils;
@Inject
private SelectedRevisions selectedRevisions;
private ChangeInfo changeInfo;
private String selectedRevisionId;
private Optional<Pair<String, RevisionInfo>> baseRevision;
private Project project;
@Override
public boolean canShow(DiffRequest request) {
final boolean superCanShow = super.canShow(request);
final AsyncResult<DataContext> dataContextFromFocus = dataManager.getDataContextFromFocus();
final DataContext context = dataContextFromFocus.getResult();
if (context == null) return false;
changeInfo = GerritDataKeys.CHANGE.getData(context);
if (changeInfo != null) {
selectedRevisionId = selectedRevisions.get(changeInfo);
} else {
selectedRevisionId = null;
}
baseRevision = GerritDataKeys.BASE_REVISION.getData(context);
project = PlatformDataKeys.PROJECT.getData(context);
return superCanShow && changeInfo != null;
}
@Override
public void diffRequestChange(DiffRequest diffRequest, DiffPanelImpl diffPanel) {
handleComments(diffPanel, diffRequest.getWindowTitle());
}
private void handleComments(final DiffPanelImpl diffPanel, final String filePathString) {
final FilePath filePath = new FilePathImpl(new File(filePathString), false);
final String relativeFilePath = PathUtils.ensureSlashSeparators(getRelativeOrAbsolutePath(project, filePath.getPath()));
addCommentAction(diffPanel, relativeFilePath, changeInfo);
gerritUtil.getChangeDetails(changeInfo._number, project, new Consumer<ChangeInfo>() {
@Override
public void consume(ChangeInfo changeDetails) {
gerritUtil.getComments(changeDetails.id, selectedRevisionId, project, true, true,
new Consumer<Map<String, List<CommentInfo>>>() {
@Override
public void consume(Map<String, List<CommentInfo>> comments) {
List<CommentInfo> fileComments = comments.get(relativeFilePath);
if (fileComments != null) {
addCommentsGutter(
diffPanel.getEditor2(),
relativeFilePath,
selectedRevisionId,
Iterables.filter(fileComments, REVISION_COMMENT)
);
if (!baseRevision.isPresent()) {
addCommentsGutter(
diffPanel.getEditor1(),
relativeFilePath,
selectedRevisionId,
Iterables.filter(fileComments, Predicates.not(REVISION_COMMENT))
);
}
}
}
}
);
if (baseRevision.isPresent()) {
gerritUtil.getComments(changeDetails.id, baseRevision.get().getFirst(), project, true, true,
new Consumer<Map<String, List<CommentInfo>>>() {
@Override
public void consume(Map<String, List<CommentInfo>> comments) {
List<CommentInfo> fileComments = comments.get(relativeFilePath);
if (fileComments != null) {
Collections.sort(fileComments, COMMENT_ORDERING);
addCommentsGutter(
diffPanel.getEditor1(),
relativeFilePath,
baseRevision.get().getFirst(),
Iterables.filter(fileComments, REVISION_COMMENT)
);
}
}
});
}
gerritUtil.setReviewed(changeDetails.id, selectedRevisionId,
relativeFilePath, project);
}
});
}
private void addCommentAction(DiffPanelImpl diffPanel, String filePath, ChangeInfo changeInfo) {
if (baseRevision.isPresent()) {
addCommentActionToEditor(diffPanel.getEditor1(), filePath, changeInfo, baseRevision.get().getFirst(), Comment.Side.REVISION);
} else {
addCommentActionToEditor(diffPanel.getEditor1(), filePath, changeInfo, selectedRevisionId, Comment.Side.PARENT);
}
addCommentActionToEditor(diffPanel.getEditor2(), filePath, changeInfo, selectedRevisionId, Comment.Side.REVISION);
}
private void addCommentActionToEditor(Editor editor, String filePath, ChangeInfo changeInfo, String revisionId, Comment.Side commentSide) {
if (editor == null) return;
DefaultActionGroup group = new DefaultActionGroup();
final AddCommentAction addCommentAction = addCommentActionBuilder
.create(this, changeInfo, revisionId, editor, filePath, commentSide)
.withText("Add Comment")
.withIcon(AllIcons.Toolwindows.ToolWindowMessages)
.get();
addCommentAction.registerCustomShortcutSet(CustomShortcutSet.fromString("C"), editor.getContentComponent());
group.add(addCommentAction);
PopupHandler.installUnknownPopupHandler(editor.getContentComponent(), group, ActionManager.getInstance());
}
private void addCommentsGutter(Editor editor,
String filePath,
String revisionId,
Iterable<CommentInfo> fileComments) {
for (CommentInfo fileComment : fileComments) {
fileComment.path = PathUtils.ensureSlashSeparators(filePath);
addComment(editor, changeInfo, revisionId, project, fileComment);
}
}
public void addComment(Editor editor, ChangeInfo changeInfo, String revisionId, Project project, Comment comment) {
if (editor == null) return;
MarkupModel markup = editor.getMarkupModel();
RangeHighlighter rangeHighlighter = null;
if (comment.range != null) {
rangeHighlighter = highlightRangeComment(comment.range, editor, project);
}
int lineCount = markup.getDocument().getLineCount();
int line = comment.line - 1;
if (line < 0) {
line = 0;
}
if (line > lineCount - 1) {
line = lineCount - 1;
}
if (line >= 0) {
final RangeHighlighter highlighter = markup.addLineHighlighter(line, HighlighterLayer.ERROR + 1, null);
CommentGutterIconRenderer iconRenderer = new CommentGutterIconRenderer(
this, editor, gerritUtil, selectedRevisions, addCommentActionBuilder,
comment, changeInfo, revisionId, highlighter, rangeHighlighter);
highlighter.setGutterIconRenderer(iconRenderer);
}
}
public void removeComment(Editor editor, RangeHighlighter lineHighlighter, RangeHighlighter rangeHighlighter) {
editor.getMarkupModel().removeHighlighter(lineHighlighter);
lineHighlighter.dispose();
if (rangeHighlighter != null) {
HighlightManager highlightManager = HighlightManager.getInstance(project);
highlightManager.removeSegmentHighlighter(editor, rangeHighlighter);
}
}
private String getRelativeOrAbsolutePath(Project project, String absoluteFilePath) {
return pathUtils.getRelativeOrAbsolutePath(project, absoluteFilePath, changeInfo.project);
}
public static RangeHighlighter highlightRangeComment(Comment.Range range, Editor editor, Project project) {
CharSequence charsSequence = editor.getMarkupModel().getDocument().getCharsSequence();
RangeUtils.Offset offset = RangeUtils.rangeToTextOffset(charsSequence, range);
TextAttributes attributes = new TextAttributes();
attributes.setBackgroundColor(JBColor.YELLOW);
ArrayList<RangeHighlighter> highlighters = Lists.newArrayList();
HighlightManager highlightManager = HighlightManager.getInstance(project);
highlightManager.addRangeHighlight(editor, offset.start, offset.end, attributes, false, highlighters);
return highlighters.get(0);
}
}