diff: add annotate action to diff viewers
authorAleksey Pivovarov <AMPivovarov@gmail.com>
Sat, 20 Jun 2015 13:16:08 +0000 (16:16 +0300)
committerAleksey Pivovarov <AMPivovarov@gmail.com>
Thu, 3 Sep 2015 16:28:06 +0000 (19:28 +0300)
platform/diff-impl/src/com/intellij/diff/tools/fragmented/UnifiedDiffViewer.java
platform/platform-resources/src/idea/VcsActions.xml
platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotateDiffViewerAction.java [new file with mode: 0644]
platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotateToggleAction.java

index d54b5158f70f2cd2c0b755dafdbcddd80be680fd..f3eb5b650c993f2854b46b8cd3205aa723fbcc55 100644 (file)
@@ -459,7 +459,7 @@ public class UnifiedDiffViewer extends ListenerDiffViewerBase {
    * This convertor returns -1 if exact matching is impossible
    */
   @CalledInAwt
-  protected int transferLineToOnesideStrict(@NotNull Side side, int line) {
+  public int transferLineToOnesideStrict(@NotNull Side side, int line) {
     if (myChangedBlockData == null) return -1;
 
     LineNumberConvertor lineConvertor = myChangedBlockData.getLineNumberConvertor();
@@ -470,7 +470,7 @@ public class UnifiedDiffViewer extends ListenerDiffViewerBase {
    * This convertor returns -1 if exact matching is impossible
    */
   @CalledInAwt
-  protected int transferLineFromOnesideStrict(@NotNull Side side, int line) {
+  public int transferLineFromOnesideStrict(@NotNull Side side, int line) {
     if (myChangedBlockData == null) return -1;
 
     LineNumberConvertor lineConvertor = myChangedBlockData.getLineNumberConvertor();
@@ -481,7 +481,7 @@ public class UnifiedDiffViewer extends ListenerDiffViewerBase {
    * This convertor returns 'good enough' position, even if exact matching is impossible
    */
   @CalledInAwt
-  protected int transferLineToOneside(@NotNull Side side, int line) {
+  public int transferLineToOneside(@NotNull Side side, int line) {
     if (myChangedBlockData == null) return line;
 
     LineNumberConvertor lineConvertor = myChangedBlockData.getLineNumberConvertor();
@@ -493,7 +493,7 @@ public class UnifiedDiffViewer extends ListenerDiffViewerBase {
    */
   @CalledInAwt
   @NotNull
-  protected Pair<int[], Side> transferLineFromOneside(int line) {
+  public Pair<int[], Side> transferLineFromOneside(int line) {
     int[] lines = new int[2];
 
     if (myChangedBlockData == null) {
@@ -839,6 +839,12 @@ public class UnifiedDiffViewer extends ListenerDiffViewerBase {
   // Getters
   //
 
+
+  @NotNull
+  public Side getMasterSide() {
+    return myMasterSide;
+  }
+
   @NotNull
   public EditorEx getEditor() {
     return myEditor;
index 88d95bea55cd15d9f0ad9991f4099512cbc3bd29..eb767a7dde98eca2be7f014aee979578eb6e9f85 100644 (file)
@@ -11,6 +11,9 @@
     <action id="CheckStatusForFiles" class="com.intellij.openapi.vcs.update.CommonStatusFileOrDirectoryAction"/>
     <action id="IntegrateFiles" class="com.intellij.openapi.vcs.update.CommonIntegrateFileOrDirectoryAction"/>
     <action id="Annotate" class="com.intellij.openapi.vcs.actions.AnnotateToggleAction"/>
+    <action id="AnnotateDiffViewer" class="com.intellij.openapi.vcs.actions.AnnotateDiffViewerAction">
+      <add-to-group group-id="Diff.EditorPopupMenu"/>
+    </action>
     <action id="Show.Current.Revision" class="com.intellij.openapi.vcs.actions.ShowBaseRevisionAction" text="Show Current Revision"/>
     <action id="Compare.SameVersion" class="com.intellij.openapi.vcs.actions.CompareWithTheSameVersionAction" icon="AllIcons.Actions.Diff"/>
     <action id="Compare.LastVersion" class="com.intellij.openapi.vcs.actions.CompareWithLastVersion"/>
diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotateDiffViewerAction.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/actions/AnnotateDiffViewerAction.java
new file mode 100644 (file)
index 0000000..d348ae3
--- /dev/null
@@ -0,0 +1,400 @@
+/*
+ * Copyright 2000-2015 JetBrains s.r.o.
+ *
+ * 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.intellij.openapi.vcs.actions;
+
+import com.intellij.diff.FrameDiffTool.DiffViewer;
+import com.intellij.diff.contents.DiffContent;
+import com.intellij.diff.contents.FileContent;
+import com.intellij.diff.requests.ContentDiffRequest;
+import com.intellij.diff.requests.DiffRequest;
+import com.intellij.diff.tools.fragmented.UnifiedDiffViewer;
+import com.intellij.diff.tools.util.DiffDataKeys;
+import com.intellij.diff.tools.util.base.DiffViewerBase;
+import com.intellij.diff.tools.util.side.OnesideTextDiffViewer;
+import com.intellij.diff.tools.util.side.TwosideTextDiffViewer;
+import com.intellij.diff.util.Side;
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.localVcs.UpToDateLineNumberProvider;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.progress.ProgressManager;
+import com.intellij.openapi.progress.Task;
+import com.intellij.openapi.project.DumbAwareAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Pair;
+import com.intellij.openapi.vcs.*;
+import com.intellij.openapi.vcs.annotate.AnnotationProvider;
+import com.intellij.openapi.vcs.annotate.FileAnnotation;
+import com.intellij.openapi.vcs.changes.*;
+import com.intellij.openapi.vcs.changes.actions.diff.ChangeDiffRequestProducer;
+import com.intellij.openapi.vcs.impl.BackgroundableActionEnabledHandler;
+import com.intellij.openapi.vcs.impl.ProjectLevelVcsManagerImpl;
+import com.intellij.openapi.vcs.impl.UpToDateLineNumberProviderImpl;
+import com.intellij.openapi.vcs.impl.VcsBackgroundableActions;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.ObjectUtils;
+import com.intellij.vcs.AnnotationProviderEx;
+import com.intellij.vcsUtil.VcsUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class AnnotateDiffViewerAction extends DumbAwareAction {
+  private static final ViewerAnnotator[] ANNOTATORS = new ViewerAnnotator[]{
+    new TwosideAnnotator(), new OnesideAnnotator(), new UnifiedAnnotator()
+  };
+
+  public AnnotateDiffViewerAction() {
+    super("Annotate", null, AllIcons.Actions.Annotate);
+  }
+
+  @Override
+  public void update(AnActionEvent e) {
+    e.getPresentation().setEnabledAndVisible(isEnabled(e));
+  }
+
+  @Nullable
+  private static ViewerAnnotator getAnnotator(@NotNull DiffViewerBase viewer) {
+    for (ViewerAnnotator annotator : ANNOTATORS) {
+      if (annotator.getViewerClass().isInstance(viewer)) return annotator;
+    }
+    return null;
+  }
+
+  private static boolean isEnabled(AnActionEvent e) {
+    DiffViewerBase viewer = ObjectUtils.tryCast(e.getData(DiffDataKeys.DIFF_VIEWER), DiffViewerBase.class);
+    if (viewer == null) return false;
+    if (viewer.getProject() == null) return false;
+    if (viewer.isDisposed()) return false;
+
+    Editor editor = e.getData(CommonDataKeys.EDITOR);
+    if (editor == null) return false;
+
+    ViewerAnnotator annotator = getAnnotator(viewer);
+    if (annotator == null) return false;
+
+    //noinspection unchecked
+    Side side = annotator.getCurrentSide(viewer, editor);
+    if (side == null) return false;
+
+    //noinspection unchecked
+    if (annotator.isAnnotationShown(viewer, side)) return false;
+    if (checkRunningProgress(viewer, side)) return false;
+    return createAnnotationsLoader(viewer.getProject(), viewer.getRequest(), side) != null;
+  }
+
+  @Override
+  public void actionPerformed(final AnActionEvent e) {
+    DiffViewerBase viewer = (DiffViewerBase)e.getRequiredData(DiffDataKeys.DIFF_VIEWER);
+    Editor editor = e.getRequiredData(CommonDataKeys.EDITOR);
+
+    ViewerAnnotator annotator = getAnnotator(viewer);
+    assert annotator != null;
+
+    //noinspection unchecked
+    Side side = annotator.getCurrentSide(viewer, editor);
+    assert side != null;
+
+    doAnnotate(annotator, viewer, side);
+  }
+
+  public static <T extends DiffViewerBase> void doAnnotate(@NotNull final ViewerAnnotator<T> annotator,
+                                                           @NotNull final T viewer,
+                                                           @NotNull final Side side) {
+    final Project project = viewer.getProject();
+    assert project != null;
+
+    final FileAnnotationLoader loader = createAnnotationsLoader(project, viewer.getRequest(), side);
+    assert loader != null;
+
+    markRunningProgress(viewer, side, true);
+
+    // TODO: show progress in diff viewer
+    ProgressManager.getInstance().run(new Task.Backgroundable(project, VcsBundle.message("retrieving.annotations"), true,
+                                                              BackgroundFromStartOption.getInstance()) {
+      public void run(@NotNull ProgressIndicator indicator) {
+        loader.run();
+      }
+
+      @Override
+      public void onCancel() {
+        onSuccess();
+      }
+
+      @Override
+      public void onSuccess() {
+        markRunningProgress(viewer, side, false);
+
+        if (loader.getException() != null) {
+          AbstractVcsHelper.getInstance(myProject).showError(loader.getException(), VcsBundle.message("operation.name.annotate"));
+        }
+        if (loader.getResult() != null) {
+          if (viewer.isDisposed()) return;
+          annotator.showAnnotation(viewer, side, loader.getResult());
+        }
+      }
+    });
+  }
+
+  @Nullable
+  private static FileAnnotationLoader createAnnotationsLoader(@NotNull Project project, @NotNull DiffRequest request, @NotNull Side side) {
+    Change change = request.getUserData(ChangeDiffRequestProducer.CHANGE_KEY);
+    if (change != null) {
+      final ContentRevision revision = side.select(change.getBeforeRevision(), change.getAfterRevision());
+      if (revision == null) return null;
+      AbstractVcs vcs = ChangesUtil.getVcsForChange(change, project);
+      if (vcs == null) return null;
+
+      final AnnotationProvider annotationProvider = vcs.getAnnotationProvider();
+      if (annotationProvider == null) return null;
+
+      if (revision instanceof CurrentContentRevision) {
+        return new FileAnnotationLoader(vcs) {
+          @Override
+          public FileAnnotation compute() throws VcsException {
+            final VirtualFile file = ((CurrentContentRevision)revision).getVirtualFile();
+            if (file == null) throw new VcsException("Failed to annotate: file not found");
+            return annotationProvider.annotate(file);
+          }
+        };
+      }
+      else {
+        if (!(annotationProvider instanceof AnnotationProviderEx)) return null;
+        return new FileAnnotationLoader(vcs) {
+          @Override
+          public FileAnnotation compute() throws VcsException {
+            return ((AnnotationProviderEx)annotationProvider).annotate(revision.getFile(), revision.getRevisionNumber());
+          }
+        };
+      }
+    }
+
+    if (request instanceof ContentDiffRequest) {
+      ContentDiffRequest requestEx = (ContentDiffRequest)request;
+      if (requestEx.getContents().size() != 2) return null;
+      DiffContent content = side.select(requestEx.getContents());
+      if (content instanceof FileContent) {
+        final VirtualFile file = ((FileContent)content).getFile();
+        AbstractVcs vcs = VcsUtil.getVcsFor(project, file);
+        if (vcs == null) return null;
+
+        final AnnotationProvider annotationProvider = vcs.getAnnotationProvider();
+        if (annotationProvider == null) return null;
+
+        return new FileAnnotationLoader(vcs) {
+          @Override
+          public FileAnnotation compute() throws VcsException {
+            return annotationProvider.annotate(file);
+          }
+        };
+      }
+    }
+
+    return null;
+  }
+
+  private static class TwosideAnnotator extends ViewerAnnotator<TwosideTextDiffViewer> {
+    @Override
+    @NotNull
+    public Class<TwosideTextDiffViewer> getViewerClass() {
+      return TwosideTextDiffViewer.class;
+    }
+
+    @Override
+    @Nullable
+    public Side getCurrentSide(@NotNull TwosideTextDiffViewer viewer, @NotNull Editor editor) {
+      Side side = null; // we can't just use getCurrentSide() here, popup can be called on unfocused editor
+      if (viewer.getEditor(Side.LEFT) == editor) side = Side.LEFT;
+      if (viewer.getEditor(Side.RIGHT) == editor) side = Side.RIGHT;
+      return side;
+    }
+
+    @Override
+    public boolean isAnnotationShown(@NotNull TwosideTextDiffViewer viewer, @NotNull Side side) {
+      return viewer.getEditor(side).getGutter().isAnnotationsShown();
+    }
+
+    @Override
+    public void showAnnotation(@NotNull TwosideTextDiffViewer viewer, @NotNull Side side, @NotNull AnnotationData data) {
+      AnnotateToggleAction.doAnnotate(viewer.getEditor(side), viewer.getProject(), null, data.annotation, data.vcs, null);
+    }
+  }
+
+  private static class OnesideAnnotator extends ViewerAnnotator<OnesideTextDiffViewer> {
+    @Override
+    @NotNull
+    public Class<OnesideTextDiffViewer> getViewerClass() {
+      return OnesideTextDiffViewer.class;
+    }
+
+    @Override
+    @Nullable
+    public Side getCurrentSide(@NotNull OnesideTextDiffViewer viewer, @NotNull Editor editor) {
+      if (viewer.getEditor() != editor) return null;
+      return viewer.getSide();
+    }
+
+    @Override
+    public boolean isAnnotationShown(@NotNull OnesideTextDiffViewer viewer, @NotNull Side side) {
+      if (side != viewer.getSide()) return false;
+      return viewer.getEditor().getGutter().isAnnotationsShown();
+    }
+
+    @Override
+    public void showAnnotation(@NotNull OnesideTextDiffViewer viewer, @NotNull Side side, @NotNull AnnotationData data) {
+      if (side != viewer.getSide()) return;
+      AnnotateToggleAction.doAnnotate(viewer.getEditor(), viewer.getProject(), null, data.annotation, data.vcs, null);
+    }
+  }
+
+  private static class UnifiedAnnotator extends ViewerAnnotator<UnifiedDiffViewer> {
+    @Override
+    @NotNull
+    public Class<UnifiedDiffViewer> getViewerClass() {
+      return UnifiedDiffViewer.class;
+    }
+
+    @Override
+    @Nullable
+    public Side getCurrentSide(@NotNull UnifiedDiffViewer viewer, @NotNull Editor editor) {
+      if (viewer.getEditor() != editor) return null;
+      return viewer.getMasterSide();
+    }
+
+    @Override
+    public boolean isAnnotationShown(@NotNull UnifiedDiffViewer viewer, @NotNull Side side) {
+      if (side != viewer.getMasterSide()) return false;
+      return viewer.getEditor().getGutter().isAnnotationsShown();
+    }
+
+    @Override
+    public void showAnnotation(@NotNull UnifiedDiffViewer viewer, @NotNull Side side, @NotNull AnnotationData data) {
+      if (side != viewer.getMasterSide()) return;
+      UnifiedUpToDateLineNumberProvider lineNumberProvider = new UnifiedUpToDateLineNumberProvider(viewer, side);
+      AnnotateToggleAction.doAnnotate(viewer.getEditor(), viewer.getProject(), null, data.annotation, data.vcs, lineNumberProvider);
+    }
+  }
+
+  private static class UnifiedUpToDateLineNumberProvider implements UpToDateLineNumberProvider {
+    @NotNull private final UnifiedDiffViewer myViewer;
+    @NotNull private final Side mySide;
+    @NotNull private final UpToDateLineNumberProvider myLocalChangesProvider;
+
+    public UnifiedUpToDateLineNumberProvider(@NotNull UnifiedDiffViewer viewer, @NotNull Side side) {
+      myViewer = viewer;
+      mySide = side;
+      myLocalChangesProvider = new UpToDateLineNumberProviderImpl(myViewer.getDocument(mySide), viewer.getProject());
+    }
+
+    @Override
+    public int getLineNumber(int currentNumber) {
+      int number = myViewer.transferLineFromOnesideStrict(mySide, currentNumber);
+      return number != -1 ? myLocalChangesProvider.getLineNumber(number) : -1;
+    }
+
+    @Override
+    public boolean isLineChanged(int currentNumber) {
+      return getLineNumber(currentNumber) == -1;
+    }
+
+    @Override
+    public boolean isRangeChanged(int start, int end) {
+      int line1 = myViewer.transferLineFromOnesideStrict(mySide, start);
+      int line2 = myViewer.transferLineFromOnesideStrict(mySide, end);
+      if (line2 - line1 != end - start) return true;
+
+      for (int i = start; i <= end; i++) {
+        if (isLineChanged(i)) return true; // TODO: a single request to LineNumberConvertor
+      }
+      return myLocalChangesProvider.isRangeChanged(line1, line2);
+    }
+  }
+
+  private static abstract class ViewerAnnotator<T extends DiffViewerBase> {
+    @NotNull
+    public abstract Class<T> getViewerClass();
+
+    @Nullable
+    public abstract Side getCurrentSide(@NotNull T viewer, @NotNull Editor editor);
+
+    public abstract boolean isAnnotationShown(@NotNull T viewer, @NotNull Side side);
+
+    public abstract void showAnnotation(@NotNull T viewer, @NotNull Side side, @NotNull AnnotationData data);
+  }
+
+  private abstract static class FileAnnotationLoader {
+    @NotNull private final AbstractVcs myVcs;
+
+    private VcsException myException;
+    private FileAnnotation myResult;
+
+    public FileAnnotationLoader(@NotNull AbstractVcs vcs) {
+      myVcs = vcs;
+    }
+
+    public VcsException getException() {
+      return myException;
+    }
+
+    public AnnotationData getResult() {
+      return new AnnotationData(myVcs, myResult);
+    }
+
+    public void run() {
+      try {
+        myResult = compute();
+      }
+      catch (VcsException e) {
+        myException = e;
+      }
+    }
+
+    protected abstract FileAnnotation compute() throws VcsException;
+  }
+
+  private static class AnnotationData {
+    @NotNull public final AbstractVcs vcs;
+    @NotNull public final FileAnnotation annotation;
+
+    public AnnotationData(@NotNull AbstractVcs vcs, @NotNull FileAnnotation annotation) {
+      this.vcs = vcs;
+      this.annotation = annotation;
+    }
+  }
+
+  private static boolean checkRunningProgress(@NotNull DiffViewerBase viewer, @NotNull Side side) {
+    final ProjectLevelVcsManagerImpl plVcsManager = (ProjectLevelVcsManagerImpl)ProjectLevelVcsManager.getInstance(viewer.getProject());
+    final BackgroundableActionEnabledHandler handler = plVcsManager.getBackgroundableActionHandler(VcsBackgroundableActions.ANNOTATE);
+    return handler.isInProgress(key(viewer, side));
+  }
+
+  private static void markRunningProgress(@NotNull DiffViewerBase viewer, @NotNull Side side, boolean running) {
+    final ProjectLevelVcsManagerImpl plVcsManager = (ProjectLevelVcsManagerImpl)ProjectLevelVcsManager.getInstance(viewer.getProject());
+    final BackgroundableActionEnabledHandler handler = plVcsManager.getBackgroundableActionHandler(VcsBackgroundableActions.ANNOTATE);
+    if (running) {
+      handler.register(key(viewer, side));
+    }
+    else {
+      handler.completed(key(viewer, side));
+    }
+  }
+
+  @NotNull
+  private static Object key(@NotNull DiffViewer viewer, @NotNull Side side) {
+    return Pair.create(viewer, side);
+  }
+}
index 50768a3f14a13dee21dcfde6108702646b52df6c..ca182166e500477ae94afd6e749c2cb9283d7908 100644 (file)
@@ -15,7 +15,6 @@
  */
 package com.intellij.openapi.vcs.actions;
 
-import com.intellij.openapi.actionSystem.AnAction;
 import com.intellij.openapi.actionSystem.AnActionEvent;
 import com.intellij.openapi.actionSystem.Separator;
 import com.intellij.openapi.actionSystem.ToggleAction;
@@ -221,6 +220,18 @@ public class AnnotateToggleAction extends ToggleAction implements DumbAware, Ann
                                 @NotNull final FileAnnotation fileAnnotation,
                                 @NotNull final AbstractVcs vcs,
                                 final boolean onCurrentRevision) {
+    if (onCurrentRevision) {
+      ProjectLevelVcsManager.getInstance(project).getAnnotationLocalChangesListener().registerAnnotation(fileAnnotation.getFile(), fileAnnotation);
+    }
+    doAnnotate(editor, project, currentFile, fileAnnotation, vcs, null);
+  }
+
+  public static void doAnnotate(@NotNull final Editor editor,
+                                @NotNull final Project project,
+                                @Nullable final VirtualFile currentFile,
+                                @NotNull final FileAnnotation fileAnnotation,
+                                @NotNull final AbstractVcs vcs,
+                                @Nullable UpToDateLineNumberProvider getUpToDateLineNumber) {
     editor.getGutter().closeAllAnnotations();
 
     fileAnnotation.setCloser(new Runnable() {
@@ -236,17 +247,15 @@ public class AnnotateToggleAction extends ToggleAction implements DumbAware, Ann
         });
       }
     });
-    if (onCurrentRevision) {
-      ProjectLevelVcsManager.getInstance(project).getAnnotationLocalChangesListener().registerAnnotation(fileAnnotation.getFile(), fileAnnotation);
-    }
+
 
     final EditorGutterComponentEx editorGutter = (EditorGutterComponentEx)editor.getGutter();
     final List<AnnotationFieldGutter> gutters = new ArrayList<AnnotationFieldGutter>();
     final AnnotationSourceSwitcher switcher = fileAnnotation.getAnnotationSourceSwitcher();
-    final UpToDateLineNumberProvider getUpToDateLineNumber = new UpToDateLineNumberProviderImpl(editor.getDocument(), project);
+    if (getUpToDateLineNumber == null) getUpToDateLineNumber = new UpToDateLineNumberProviderImpl(editor.getDocument(), project);
 
     final AnnotationPresentation presentation = new AnnotationPresentation(fileAnnotation, getUpToDateLineNumber, switcher);
-    if (vcs.getCommittedChangesProvider() != null) {
+    if (currentFile != null && vcs.getCommittedChangesProvider() != null) {
       presentation.addAction(new ShowDiffFromAnnotation(fileAnnotation, vcs, currentFile));
     }
     presentation.addAction(new CopyRevisionNumberFromAnnotateAction(fileAnnotation));