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