diff: show annotations in diff from history
[idea/community.git] / platform / vcs-impl / src / com / intellij / openapi / vcs / actions / AnnotateDiffViewerAction.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi.vcs.actions;
17
18 import com.intellij.diff.DiffContext;
19 import com.intellij.diff.DiffExtension;
20 import com.intellij.diff.FrameDiffTool.DiffViewer;
21 import com.intellij.diff.contents.DiffContent;
22 import com.intellij.diff.contents.FileContent;
23 import com.intellij.diff.requests.ContentDiffRequest;
24 import com.intellij.diff.requests.DiffRequest;
25 import com.intellij.diff.tools.fragmented.UnifiedDiffViewer;
26 import com.intellij.diff.tools.util.DiffDataKeys;
27 import com.intellij.diff.tools.util.base.DiffViewerBase;
28 import com.intellij.diff.tools.util.base.DiffViewerListener;
29 import com.intellij.diff.tools.util.side.OnesideTextDiffViewer;
30 import com.intellij.diff.tools.util.side.TwosideTextDiffViewer;
31 import com.intellij.diff.util.Side;
32 import com.intellij.icons.AllIcons;
33 import com.intellij.openapi.actionSystem.AnActionEvent;
34 import com.intellij.openapi.actionSystem.CommonDataKeys;
35 import com.intellij.openapi.diagnostic.Logger;
36 import com.intellij.openapi.editor.Editor;
37 import com.intellij.openapi.localVcs.UpToDateLineNumberProvider;
38 import com.intellij.openapi.progress.ProgressIndicator;
39 import com.intellij.openapi.progress.ProgressManager;
40 import com.intellij.openapi.progress.Task;
41 import com.intellij.openapi.project.DumbAwareAction;
42 import com.intellij.openapi.project.Project;
43 import com.intellij.openapi.util.Key;
44 import com.intellij.openapi.util.Pair;
45 import com.intellij.openapi.vcs.*;
46 import com.intellij.openapi.vcs.annotate.AnnotationProvider;
47 import com.intellij.openapi.vcs.annotate.FileAnnotation;
48 import com.intellij.openapi.vcs.changes.*;
49 import com.intellij.openapi.vcs.changes.actions.diff.ChangeDiffRequestProducer;
50 import com.intellij.openapi.vcs.history.VcsFileRevision;
51 import com.intellij.openapi.vcs.history.VcsFileRevisionEx;
52 import com.intellij.openapi.vcs.history.VcsHistoryUtil;
53 import com.intellij.openapi.vcs.history.VcsRevisionNumber;
54 import com.intellij.openapi.vcs.impl.BackgroundableActionEnabledHandler;
55 import com.intellij.openapi.vcs.impl.ProjectLevelVcsManagerImpl;
56 import com.intellij.openapi.vcs.impl.UpToDateLineNumberProviderImpl;
57 import com.intellij.openapi.vcs.impl.VcsBackgroundableActions;
58 import com.intellij.openapi.vfs.VirtualFile;
59 import com.intellij.util.ObjectUtils;
60 import com.intellij.vcs.AnnotationProviderEx;
61 import com.intellij.vcsUtil.VcsUtil;
62 import org.jetbrains.annotations.NotNull;
63 import org.jetbrains.annotations.Nullable;
64
65 public class AnnotateDiffViewerAction extends DumbAwareAction {
66   public static final Logger LOG = Logger.getInstance(AnnotateDiffViewerAction.class);
67
68   private static final Key<AnnotationData[]> CACHE_KEY = Key.create("Diff.AnnotateAction.Cache");
69   private static final Key<boolean[]> ANNOTATIONS_SHOWN_KEY = Key.create("Diff.AnnotateAction.AnnotationShown");
70
71   private static final ViewerAnnotator[] ANNOTATORS = new ViewerAnnotator[]{
72     new TwosideAnnotator(), new OnesideAnnotator(), new UnifiedAnnotator()
73   };
74
75   public AnnotateDiffViewerAction() {
76     super("Annotate", null, AllIcons.Actions.Annotate);
77   }
78
79   @Override
80   public void update(AnActionEvent e) {
81     e.getPresentation().setEnabledAndVisible(isEnabled(e));
82   }
83
84   @Nullable
85   private static ViewerAnnotator getAnnotator(@NotNull DiffViewerBase viewer) {
86     for (ViewerAnnotator annotator : ANNOTATORS) {
87       if (annotator.getViewerClass().isInstance(viewer)) return annotator;
88     }
89     return null;
90   }
91
92   private static boolean isEnabled(AnActionEvent e) {
93     DiffViewerBase viewer = ObjectUtils.tryCast(e.getData(DiffDataKeys.DIFF_VIEWER), DiffViewerBase.class);
94     if (viewer == null) return false;
95     if (viewer.getProject() == null) return false;
96     if (viewer.isDisposed()) return false;
97
98     Editor editor = e.getData(CommonDataKeys.EDITOR);
99     if (editor == null) return false;
100
101     ViewerAnnotator annotator = getAnnotator(viewer);
102     if (annotator == null) return false;
103
104     //noinspection unchecked
105     Side side = annotator.getCurrentSide(viewer, editor);
106     if (side == null) return false;
107
108     //noinspection unchecked
109     if (annotator.isAnnotationShown(viewer, side)) return false;
110     if (checkRunningProgress(viewer, side)) return false;
111     return createAnnotationsLoader(viewer.getProject(), viewer.getRequest(), side) != null;
112   }
113
114   @Override
115   public void actionPerformed(final AnActionEvent e) {
116     DiffViewerBase viewer = (DiffViewerBase)e.getRequiredData(DiffDataKeys.DIFF_VIEWER);
117     Editor editor = e.getRequiredData(CommonDataKeys.EDITOR);
118
119     ViewerAnnotator annotator = getAnnotator(viewer);
120     assert annotator != null;
121
122     //noinspection unchecked
123     Side side = annotator.getCurrentSide(viewer, editor);
124     assert side != null;
125
126     doAnnotate(annotator, viewer, side);
127   }
128
129   public static <T extends DiffViewerBase> void doAnnotate(@NotNull final ViewerAnnotator<T> annotator,
130                                                            @NotNull final T viewer,
131                                                            @NotNull final Side side) {
132     final Project project = viewer.getProject();
133     assert project != null;
134
135     AnnotationData data = getDataFromCache(viewer, side);
136     if (data != null) {
137       annotator.showAnnotation(viewer, side, data);
138       return;
139     }
140
141     final FileAnnotationLoader loader = createAnnotationsLoader(project, viewer.getRequest(), side);
142     assert loader != null;
143
144     markRunningProgress(viewer, side, true);
145
146     // TODO: show progress in diff viewer
147     // TODO: we can abort loading on DiffViewer.dispose(). But vcs can't stop gracefully anyway.
148     ProgressManager.getInstance().run(new Task.Backgroundable(project, VcsBundle.message("retrieving.annotations"), true,
149                                                               BackgroundFromStartOption.getInstance()) {
150       public void run(@NotNull ProgressIndicator indicator) {
151         loader.run();
152       }
153
154       @Override
155       public void onCancel() {
156         onSuccess();
157       }
158
159       @Override
160       public void onSuccess() {
161         markRunningProgress(viewer, side, false);
162
163         if (loader.getException() != null) {
164           AbstractVcsHelper.getInstance(myProject).showError(loader.getException(), VcsBundle.message("operation.name.annotate"));
165         }
166         if (loader.getResult() == null) return;
167         if (viewer.isDisposed()) return;
168
169         annotator.showAnnotation(viewer, side, loader.getResult());
170
171         if (loader.shouldCache()) {
172           putDataToCache(viewer, side, loader.getResult());
173         }
174       }
175     });
176   }
177
178   @Nullable
179   private static FileAnnotationLoader createAnnotationsLoader(@NotNull Project project, @NotNull DiffRequest request, @NotNull Side side) {
180     Change change = request.getUserData(ChangeDiffRequestProducer.CHANGE_KEY);
181     if (change != null) {
182       final ContentRevision revision = side.select(change.getBeforeRevision(), change.getAfterRevision());
183       if (revision == null) return null;
184       AbstractVcs vcs = ChangesUtil.getVcsForChange(change, project);
185       if (vcs == null) return null;
186
187       final AnnotationProvider annotationProvider = vcs.getAnnotationProvider();
188       if (annotationProvider == null) return null;
189
190       if (revision instanceof CurrentContentRevision) {
191         return new FileAnnotationLoader(vcs, false) {
192           @Override
193           public FileAnnotation compute() throws VcsException {
194             final VirtualFile file = ((CurrentContentRevision)revision).getVirtualFile();
195             if (file == null) throw new VcsException("Failed to annotate: file not found");
196             return annotationProvider.annotate(file);
197           }
198         };
199       }
200       else {
201         if (!(annotationProvider instanceof AnnotationProviderEx)) return null;
202         return new FileAnnotationLoader(vcs, true) {
203           @Override
204           public FileAnnotation compute() throws VcsException {
205             return ((AnnotationProviderEx)annotationProvider).annotate(revision.getFile(), revision.getRevisionNumber());
206           }
207         };
208       }
209     }
210
211     if (request instanceof ContentDiffRequest) {
212       ContentDiffRequest requestEx = (ContentDiffRequest)request;
213       if (requestEx.getContents().size() != 2) return null;
214
215       DiffContent content = side.select(requestEx.getContents());
216       if (content instanceof FileContent) {
217         final VirtualFile file = ((FileContent)content).getFile();
218         AbstractVcs vcs = VcsUtil.getVcsFor(project, file);
219         if (vcs == null) return null;
220
221         final AnnotationProvider annotationProvider = vcs.getAnnotationProvider();
222         if (annotationProvider == null) return null;
223
224         return new FileAnnotationLoader(vcs, false) {
225           @Override
226           public FileAnnotation compute() throws VcsException {
227             return annotationProvider.annotate(file);
228           }
229         };
230       }
231
232       VcsFileRevision[] fileRevisions = request.getUserData(VcsHistoryUtil.REVISIONS_KEY);
233       if (fileRevisions != null && fileRevisions.length == 2) {
234         VcsFileRevision fileRevision = side.select(fileRevisions);
235         if (fileRevision instanceof VcsFileRevisionEx) {
236           final FilePath path = ((VcsFileRevisionEx)fileRevision).getPath();
237           final VcsRevisionNumber revisionNumber = fileRevision.getRevisionNumber();
238
239           AbstractVcs vcs = VcsUtil.getVcsFor(project, path);
240           if (vcs == null) return null;
241
242           final AnnotationProvider annotationProvider = vcs.getAnnotationProvider();
243           if (!(annotationProvider instanceof AnnotationProviderEx)) return null;
244
245           return new FileAnnotationLoader(vcs, true) {
246             @Override
247             public FileAnnotation compute() throws VcsException {
248               return ((AnnotationProviderEx)annotationProvider).annotate(path, revisionNumber);
249             }
250           };
251         }
252       }
253     }
254
255     return null;
256   }
257
258   private static void putDataToCache(@NotNull DiffViewerBase viewer, @NotNull Side side, @NotNull AnnotationData data) {
259     AnnotationData[] cache = viewer.getRequest().getUserData(CACHE_KEY);
260     if (cache == null || cache.length != 2) {
261       cache = new AnnotationData[2];
262       viewer.getRequest().putUserData(CACHE_KEY, cache);
263     }
264     cache[side.getIndex()] = data;
265   }
266
267   @Nullable
268   private static AnnotationData getDataFromCache(@NotNull DiffViewerBase viewer, @NotNull Side side) {
269     AnnotationData[] cache = viewer.getRequest().getUserData(CACHE_KEY);
270     if (cache != null && cache.length == 2) {
271       return side.select(cache);
272     }
273     return null;
274   }
275
276   public static class MyDiffExtension extends DiffExtension {
277     @Override
278     public void onViewerCreated(@NotNull DiffViewer diffViewer, @NotNull DiffContext context, @NotNull DiffRequest request) {
279       if (diffViewer instanceof DiffViewerBase) {
280         DiffViewerBase viewer = (DiffViewerBase)diffViewer;
281         viewer.addListener(new MyDiffViewerListener(viewer));
282       }
283     }
284   }
285
286   private static class MyDiffViewerListener extends DiffViewerListener {
287     @NotNull private final DiffViewerBase myViewer;
288
289     public MyDiffViewerListener(@NotNull DiffViewerBase viewer) {
290       myViewer = viewer;
291     }
292
293     @Override
294     public void onInit() {
295       if (myViewer.getProject() == null) return;
296
297       boolean[] annotationsShown = myViewer.getRequest().getUserData(ANNOTATIONS_SHOWN_KEY);
298       if (annotationsShown == null || annotationsShown.length != 2) return;
299
300       ViewerAnnotator annotator = getAnnotator(myViewer);
301       if (annotator == null) return;
302
303       if (annotationsShown[0]) doAnnotate(annotator, myViewer, Side.LEFT);
304       if (annotationsShown[1]) doAnnotate(annotator, myViewer, Side.RIGHT);
305     }
306
307     @Override
308     @SuppressWarnings("unchecked")
309     public void onDispose() {
310       ViewerAnnotator annotator = getAnnotator(myViewer);
311       if (annotator == null) return;
312
313       boolean[] annotationsShown = new boolean[2];
314       annotationsShown[0] = annotator.isAnnotationShown(myViewer, Side.LEFT);
315       annotationsShown[1] = annotator.isAnnotationShown(myViewer, Side.RIGHT);
316
317       myViewer.getRequest().putUserData(ANNOTATIONS_SHOWN_KEY, annotationsShown);
318     }
319   }
320
321   private static class TwosideAnnotator extends ViewerAnnotator<TwosideTextDiffViewer> {
322     @Override
323     @NotNull
324     public Class<TwosideTextDiffViewer> getViewerClass() {
325       return TwosideTextDiffViewer.class;
326     }
327
328     @Override
329     @Nullable
330     public Side getCurrentSide(@NotNull TwosideTextDiffViewer viewer, @NotNull Editor editor) {
331       Side side = null; // we can't just use getCurrentSide() here, popup can be called on unfocused editor
332       if (viewer.getEditor(Side.LEFT) == editor) side = Side.LEFT;
333       if (viewer.getEditor(Side.RIGHT) == editor) side = Side.RIGHT;
334       return side;
335     }
336
337     @Override
338     public boolean isAnnotationShown(@NotNull TwosideTextDiffViewer viewer, @NotNull Side side) {
339       return viewer.getEditor(side).getGutter().isAnnotationsShown();
340     }
341
342     @Override
343     public void showAnnotation(@NotNull TwosideTextDiffViewer viewer, @NotNull Side side, @NotNull AnnotationData data) {
344       Project project = ObjectUtils.assertNotNull(viewer.getProject());
345       AnnotateToggleAction.doAnnotate(viewer.getEditor(side), project, null, data.annotation, data.vcs, null);
346     }
347   }
348
349   private static class OnesideAnnotator extends ViewerAnnotator<OnesideTextDiffViewer> {
350     @Override
351     @NotNull
352     public Class<OnesideTextDiffViewer> getViewerClass() {
353       return OnesideTextDiffViewer.class;
354     }
355
356     @Override
357     @Nullable
358     public Side getCurrentSide(@NotNull OnesideTextDiffViewer viewer, @NotNull Editor editor) {
359       if (viewer.getEditor() != editor) return null;
360       return viewer.getSide();
361     }
362
363     @Override
364     public boolean isAnnotationShown(@NotNull OnesideTextDiffViewer viewer, @NotNull Side side) {
365       if (side != viewer.getSide()) return false;
366       return viewer.getEditor().getGutter().isAnnotationsShown();
367     }
368
369     @Override
370     public void showAnnotation(@NotNull OnesideTextDiffViewer viewer, @NotNull Side side, @NotNull AnnotationData data) {
371       if (side != viewer.getSide()) return;
372       Project project = ObjectUtils.assertNotNull(viewer.getProject());
373       AnnotateToggleAction.doAnnotate(viewer.getEditor(), project, null, data.annotation, data.vcs, null);
374     }
375   }
376
377   private static class UnifiedAnnotator extends ViewerAnnotator<UnifiedDiffViewer> {
378     @Override
379     @NotNull
380     public Class<UnifiedDiffViewer> getViewerClass() {
381       return UnifiedDiffViewer.class;
382     }
383
384     @Override
385     @Nullable
386     public Side getCurrentSide(@NotNull UnifiedDiffViewer viewer, @NotNull Editor editor) {
387       if (viewer.getEditor() != editor) return null;
388       return viewer.getMasterSide();
389     }
390
391     @Override
392     public boolean isAnnotationShown(@NotNull UnifiedDiffViewer viewer, @NotNull Side side) {
393       if (side != viewer.getMasterSide()) return false;
394       return viewer.getEditor().getGutter().isAnnotationsShown();
395     }
396
397     @Override
398     public void showAnnotation(@NotNull UnifiedDiffViewer viewer, @NotNull Side side, @NotNull AnnotationData data) {
399       if (side != viewer.getMasterSide()) return;
400       Project project = ObjectUtils.assertNotNull(viewer.getProject());
401       UnifiedUpToDateLineNumberProvider lineNumberProvider = new UnifiedUpToDateLineNumberProvider(viewer, side);
402       AnnotateToggleAction.doAnnotate(viewer.getEditor(), project, null, data.annotation, data.vcs, lineNumberProvider);
403     }
404   }
405
406   private static class UnifiedUpToDateLineNumberProvider implements UpToDateLineNumberProvider {
407     @NotNull private final UnifiedDiffViewer myViewer;
408     @NotNull private final Side mySide;
409     @NotNull private final UpToDateLineNumberProvider myLocalChangesProvider;
410
411     public UnifiedUpToDateLineNumberProvider(@NotNull UnifiedDiffViewer viewer, @NotNull Side side) {
412       myViewer = viewer;
413       mySide = side;
414       myLocalChangesProvider = new UpToDateLineNumberProviderImpl(myViewer.getDocument(mySide), viewer.getProject());
415     }
416
417     @Override
418     public int getLineNumber(int currentNumber) {
419       int number = myViewer.transferLineFromOnesideStrict(mySide, currentNumber);
420       return number != -1 ? myLocalChangesProvider.getLineNumber(number) : -1;
421     }
422
423     @Override
424     public boolean isLineChanged(int currentNumber) {
425       return getLineNumber(currentNumber) == -1;
426     }
427
428     @Override
429     public boolean isRangeChanged(int start, int end) {
430       int line1 = myViewer.transferLineFromOnesideStrict(mySide, start);
431       int line2 = myViewer.transferLineFromOnesideStrict(mySide, end);
432       if (line2 - line1 != end - start) return true;
433
434       for (int i = start; i <= end; i++) {
435         if (isLineChanged(i)) return true; // TODO: a single request to LineNumberConvertor
436       }
437       return myLocalChangesProvider.isRangeChanged(line1, line2);
438     }
439   }
440
441   private static abstract class ViewerAnnotator<T extends DiffViewerBase> {
442     @NotNull
443     public abstract Class<T> getViewerClass();
444
445     @Nullable
446     public abstract Side getCurrentSide(@NotNull T viewer, @NotNull Editor editor);
447
448     public abstract boolean isAnnotationShown(@NotNull T viewer, @NotNull Side side);
449
450     public abstract void showAnnotation(@NotNull T viewer, @NotNull Side side, @NotNull AnnotationData data);
451   }
452
453   private abstract static class FileAnnotationLoader {
454     @NotNull private final AbstractVcs myVcs;
455     private final boolean myShouldCache;
456
457     private VcsException myException;
458     private FileAnnotation myResult;
459
460     public FileAnnotationLoader(@NotNull AbstractVcs vcs, boolean cache) {
461       myVcs = vcs;
462       myShouldCache = cache;
463     }
464
465     public VcsException getException() {
466       return myException;
467     }
468
469     public AnnotationData getResult() {
470       return new AnnotationData(myVcs, myResult);
471     }
472
473     public boolean shouldCache() {
474       return myShouldCache;
475     }
476
477     public void run() {
478       try {
479         myResult = compute();
480       }
481       catch (VcsException e) {
482         myException = e;
483       }
484     }
485
486     protected abstract FileAnnotation compute() throws VcsException;
487   }
488
489   private static class AnnotationData {
490     @NotNull public final AbstractVcs vcs;
491     @NotNull public final FileAnnotation annotation;
492
493     public AnnotationData(@NotNull AbstractVcs vcs, @NotNull FileAnnotation annotation) {
494       this.vcs = vcs;
495       this.annotation = annotation;
496     }
497   }
498
499   private static boolean checkRunningProgress(@NotNull DiffViewerBase viewer, @NotNull Side side) {
500     final ProjectLevelVcsManagerImpl plVcsManager = (ProjectLevelVcsManagerImpl)ProjectLevelVcsManager.getInstance(viewer.getProject());
501     final BackgroundableActionEnabledHandler handler = plVcsManager.getBackgroundableActionHandler(VcsBackgroundableActions.ANNOTATE);
502     return handler.isInProgress(key(viewer, side));
503   }
504
505   private static void markRunningProgress(@NotNull DiffViewerBase viewer, @NotNull Side side, boolean running) {
506     final ProjectLevelVcsManagerImpl plVcsManager = (ProjectLevelVcsManagerImpl)ProjectLevelVcsManager.getInstance(viewer.getProject());
507     final BackgroundableActionEnabledHandler handler = plVcsManager.getBackgroundableActionHandler(VcsBackgroundableActions.ANNOTATE);
508     if (running) {
509       handler.register(key(viewer, side));
510     }
511     else {
512       handler.completed(key(viewer, side));
513     }
514   }
515
516   @NotNull
517   private static Object key(@NotNull DiffViewer viewer, @NotNull Side side) {
518     return Pair.create(viewer, side);
519   }
520 }