/* * 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.client.Comment; import com.google.gerrit.extensions.client.Side; import com.google.gerrit.extensions.common.ChangeInfo; 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.Disposable; import com.intellij.openapi.actionSystem.ActionManager; import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.actionSystem.DefaultActionGroup; import com.intellij.openapi.diff.DiffRequest; import com.intellij.openapi.diff.impl.DiffPanelImpl; import com.intellij.openapi.diff.impl.external.DiffManagerImpl; import com.intellij.openapi.diff.impl.external.FrameDiffTool; 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.util.Disposer; import com.intellij.openapi.vcs.FilePath; import com.intellij.openapi.vcs.FilePathImpl; import com.intellij.openapi.vcs.VcsDataKeys; import com.intellij.openapi.vcs.changes.ChangeRequestChain; import com.intellij.openapi.vcs.changes.actions.DiffRequestPresentable; import com.intellij.ui.JBColor; import com.intellij.ui.PopupHandler; import com.intellij.util.Consumer; import com.urswolfer.intellij.plugin.gerrit.GerritSettings; 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 org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.awt.*; 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 FrameDiffTool { private static final Predicate<Comment> REVISION_COMMENT = new Predicate<Comment>() { @Override public boolean apply(Comment comment) { return comment.side == null || comment.side.equals(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 GerritSettings gerritSettings; @Inject private DataManager dataManager; @Inject private AddCommentActionBuilder addCommentActionBuilder; @Inject private PathUtils pathUtils; @Inject private SelectedRevisions selectedRevisions; @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 changeInfo = GerritDataKeys.CHANGE.getData(context); return superCanShow && changeInfo != null; } @Nullable @Override protected DiffPanelImpl createDiffPanelImpl(@NotNull DiffRequest request, @Nullable Window window, @NotNull Disposable parentDisposable) { DataContext context = dataManager.getDataContextFromFocus().getResult(); ChangeInfo changeInfo = GerritDataKeys.CHANGE.getData(context); String selectedRevisionId; if (changeInfo != null) { selectedRevisionId = selectedRevisions.get(changeInfo); } else { selectedRevisionId = null; } Optional<Pair<String, RevisionInfo>> baseRevision = GerritDataKeys.BASE_REVISION.getData(context); DiffPanelImpl diffPanel = new CommentableDiffPanel(window, request, changeInfo, selectedRevisionId, baseRevision); diffPanel.setDiffRequest(request); Disposer.register(parentDisposable, diffPanel); return diffPanel; } private void handleComments(final DiffPanelImpl diffPanel, final String filePathString, final Project project, final ChangeInfo changeInfo, final String selectedRevisionId, final Optional<Pair<String, RevisionInfo>> baseRevision) { final FilePath filePath = new FilePathImpl(new File(filePathString), false); final String relativeFilePath = PathUtils.ensureSlashSeparators(getRelativeOrAbsolutePath(project, filePath.getPath(), changeInfo)); addCommentAction(diffPanel, relativeFilePath, changeInfo, selectedRevisionId, baseRevision); gerritUtil.getComments(changeInfo._number, 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), changeInfo, project ); if (!baseRevision.isPresent()) { addCommentsGutter( diffPanel.getEditor1(), relativeFilePath, selectedRevisionId, Iterables.filter(fileComments, Predicates.not(REVISION_COMMENT)), changeInfo, project ); } } } } ); if (baseRevision.isPresent()) { gerritUtil.getComments(changeInfo._number, 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), changeInfo, project ); } } }); } gerritUtil.setReviewed(changeInfo._number, selectedRevisionId, relativeFilePath, project); } private void addCommentAction(DiffPanelImpl diffPanel, String filePath, ChangeInfo changeInfo, String selectedRevisionId, Optional<Pair<String, RevisionInfo>> baseRevision) { if (baseRevision.isPresent()) { addCommentActionToEditor(diffPanel.getEditor1(), filePath, changeInfo, baseRevision.get().getFirst(), Side.REVISION); } else { addCommentActionToEditor(diffPanel.getEditor1(), filePath, changeInfo, selectedRevisionId, Side.PARENT); } addCommentActionToEditor(diffPanel.getEditor2(), filePath, changeInfo, selectedRevisionId, Side.REVISION); } private void addCommentActionToEditor(Editor editor, String filePath, ChangeInfo changeInfo, String revisionId, 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, ChangeInfo changeInfo, Project project) { 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 != null ? comment.line : 0) - 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, gerritSettings, addCommentActionBuilder, comment, changeInfo, revisionId, highlighter, rangeHighlighter); highlighter.setGutterIconRenderer(iconRenderer); } } public void removeComment(Project project, 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 class CommentableDiffPanel extends DiffPanelImpl { private final ChangeInfo changeInfo; private final String selectedRevisionId; private final Optional<Pair<String, RevisionInfo>> baseRevision; public CommentableDiffPanel(Window window, DiffRequest request, ChangeInfo changeInfo, String selectedRevisionId, Optional<Pair<String, RevisionInfo>> baseRevision) { super(window, request.getProject(), true, true, DiffManagerImpl.FULL_DIFF_DIVIDER_POLYGONS_OFFSET, CommentsDiffTool.this); this.changeInfo = changeInfo; this.selectedRevisionId = selectedRevisionId; this.baseRevision = baseRevision; } @Override public void setDiffRequest(DiffRequest request) { super.setDiffRequest(request); Object chain = request.getGenericData().get(VcsDataKeys.DIFF_REQUEST_CHAIN.getName()); if (chain instanceof ChangeRequestChain) { DiffRequestPresentable currentRequest = ((ChangeRequestChain) chain).getCurrentRequest(); if (currentRequest != null) { String path = currentRequest.getPathPresentation(); handleComments(this, path, request.getProject(), changeInfo, selectedRevisionId, baseRevision); } } } } private String getRelativeOrAbsolutePath(Project project, String absoluteFilePath, ChangeInfo changeInfo) { 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); } }