diff: cleanup
[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       ContentRevision revision = side.select(change.getBeforeRevision(), change.getAfterRevision());
190       if (revision != null) {
191         AbstractVcs vcs = ChangesUtil.getVcsForChange(change, project);
192
193         if (revision instanceof CurrentContentRevision) {
194           VirtualFile file = ((CurrentContentRevision)revision).getVirtualFile();
195           FileAnnotationLoader loader = doCreateAnnotationsLoader(vcs, file);
196           if (loader != null) return loader;
197         }
198         else {
199           FileAnnotationLoader loader = doCreateAnnotationsLoader(vcs, revision.getFile(), revision.getRevisionNumber());
200           if (loader != null) return loader;
201         }
202       }
203     }
204
205     if (request instanceof ContentDiffRequest) {
206       ContentDiffRequest requestEx = (ContentDiffRequest)request;
207       if (requestEx.getContents().size() == 2) {
208
209         DiffContent content = side.select(requestEx.getContents());
210         if (content instanceof FileContent) {
211           VirtualFile file = ((FileContent)content).getFile();
212           AbstractVcs vcs = VcsUtil.getVcsFor(project, file);
213           FileAnnotationLoader loader = doCreateAnnotationsLoader(vcs, file);
214           if (loader != null) return loader;
215         }
216
217         VcsFileRevision[] fileRevisions = request.getUserData(VcsHistoryUtil.REVISIONS_KEY);
218         if (fileRevisions != null && fileRevisions.length == 2) {
219           VcsFileRevision fileRevision = side.select(fileRevisions);
220           if (fileRevision instanceof VcsFileRevisionEx) {
221             FilePath filePath = ((VcsFileRevisionEx)fileRevision).getPath();
222             AbstractVcs vcs = VcsUtil.getVcsFor(project, filePath);
223             FileAnnotationLoader loader = doCreateAnnotationsLoader(vcs, filePath, fileRevision.getRevisionNumber());
224             if (loader != null) return loader;
225           }
226         }
227       }
228     }
229
230     return null;
231   }
232
233   @Nullable
234   private static FileAnnotationLoader doCreateAnnotationsLoader(@Nullable AbstractVcs vcs, @Nullable final VirtualFile file) {
235     if (vcs == null || file == null) return null;
236     final AnnotationProvider annotationProvider = vcs.getAnnotationProvider();
237     if (annotationProvider == null) return null;
238
239     // TODO: cache them too, listening for ProjectLevelVcsManager.getInstance(project).getAnnotationLocalChangesListener() ?
240     return new FileAnnotationLoader(vcs, false) {
241       @Override
242       public FileAnnotation compute() throws VcsException {
243         return annotationProvider.annotate(file);
244       }
245     };
246   }
247
248   @Nullable
249   private static FileAnnotationLoader doCreateAnnotationsLoader(@Nullable AbstractVcs vcs,
250                                                                 @Nullable final FilePath path,
251                                                                 @Nullable final VcsRevisionNumber revisionNumber) {
252     if (vcs == null || path == null || revisionNumber == null) return null;
253     final AnnotationProvider annotationProvider = vcs.getAnnotationProvider();
254     if (!(annotationProvider instanceof AnnotationProviderEx)) return null;
255
256     return new FileAnnotationLoader(vcs, true) {
257       @Override
258       public FileAnnotation compute() throws VcsException {
259         return ((AnnotationProviderEx)annotationProvider).annotate(path, revisionNumber);
260       }
261     };
262   }
263
264   private static void putDataToCache(@NotNull DiffViewerBase viewer, @NotNull Side side, @NotNull AnnotationData data) {
265     AnnotationData[] cache = viewer.getRequest().getUserData(CACHE_KEY);
266     if (cache == null || cache.length != 2) {
267       cache = new AnnotationData[2];
268       viewer.getRequest().putUserData(CACHE_KEY, cache);
269     }
270     cache[side.getIndex()] = data;
271   }
272
273   @Nullable
274   private static AnnotationData getDataFromCache(@NotNull DiffViewerBase viewer, @NotNull Side side) {
275     AnnotationData[] cache = viewer.getRequest().getUserData(CACHE_KEY);
276     if (cache != null && cache.length == 2) {
277       return side.select(cache);
278     }
279     return null;
280   }
281
282   public static class MyDiffExtension extends DiffExtension {
283     @Override
284     public void onViewerCreated(@NotNull DiffViewer diffViewer, @NotNull DiffContext context, @NotNull DiffRequest request) {
285       if (diffViewer instanceof DiffViewerBase) {
286         DiffViewerBase viewer = (DiffViewerBase)diffViewer;
287         viewer.addListener(new MyDiffViewerListener(viewer));
288       }
289     }
290   }
291
292   private static class MyDiffViewerListener extends DiffViewerListener {
293     @NotNull private final DiffViewerBase myViewer;
294
295     public MyDiffViewerListener(@NotNull DiffViewerBase viewer) {
296       myViewer = viewer;
297     }
298
299     @Override
300     public void onInit() {
301       if (myViewer.getProject() == null) return;
302
303       boolean[] annotationsShown = myViewer.getRequest().getUserData(ANNOTATIONS_SHOWN_KEY);
304       if (annotationsShown == null || annotationsShown.length != 2) return;
305
306       ViewerAnnotator annotator = getAnnotator(myViewer);
307       if (annotator == null) return;
308
309       if (annotationsShown[0]) doAnnotate(annotator, myViewer, Side.LEFT);
310       if (annotationsShown[1]) doAnnotate(annotator, myViewer, Side.RIGHT);
311     }
312
313     @Override
314     @SuppressWarnings("unchecked")
315     public void onDispose() {
316       ViewerAnnotator annotator = getAnnotator(myViewer);
317       if (annotator == null) return;
318
319       boolean[] annotationsShown = new boolean[2];
320       annotationsShown[0] = annotator.isAnnotationShown(myViewer, Side.LEFT);
321       annotationsShown[1] = annotator.isAnnotationShown(myViewer, Side.RIGHT);
322
323       myViewer.getRequest().putUserData(ANNOTATIONS_SHOWN_KEY, annotationsShown);
324     }
325   }
326
327   private static class TwosideAnnotator extends ViewerAnnotator<TwosideTextDiffViewer> {
328     @Override
329     @NotNull
330     public Class<TwosideTextDiffViewer> getViewerClass() {
331       return TwosideTextDiffViewer.class;
332     }
333
334     @Override
335     @Nullable
336     public Side getCurrentSide(@NotNull TwosideTextDiffViewer viewer, @NotNull Editor editor) {
337       Side side = null; // we can't just use getCurrentSide() here, popup can be called on unfocused editor
338       if (viewer.getEditor(Side.LEFT) == editor) side = Side.LEFT;
339       if (viewer.getEditor(Side.RIGHT) == editor) side = Side.RIGHT;
340       return side;
341     }
342
343     @Override
344     public boolean isAnnotationShown(@NotNull TwosideTextDiffViewer viewer, @NotNull Side side) {
345       return viewer.getEditor(side).getGutter().isAnnotationsShown();
346     }
347
348     @Override
349     public void showAnnotation(@NotNull TwosideTextDiffViewer viewer, @NotNull Side side, @NotNull AnnotationData data) {
350       Project project = ObjectUtils.assertNotNull(viewer.getProject());
351       AnnotateToggleAction.doAnnotate(viewer.getEditor(side), project, null, data.annotation, data.vcs, null);
352     }
353   }
354
355   private static class OnesideAnnotator extends ViewerAnnotator<OnesideTextDiffViewer> {
356     @Override
357     @NotNull
358     public Class<OnesideTextDiffViewer> getViewerClass() {
359       return OnesideTextDiffViewer.class;
360     }
361
362     @Override
363     @Nullable
364     public Side getCurrentSide(@NotNull OnesideTextDiffViewer viewer, @NotNull Editor editor) {
365       if (viewer.getEditor() != editor) return null;
366       return viewer.getSide();
367     }
368
369     @Override
370     public boolean isAnnotationShown(@NotNull OnesideTextDiffViewer viewer, @NotNull Side side) {
371       if (side != viewer.getSide()) return false;
372       return viewer.getEditor().getGutter().isAnnotationsShown();
373     }
374
375     @Override
376     public void showAnnotation(@NotNull OnesideTextDiffViewer viewer, @NotNull Side side, @NotNull AnnotationData data) {
377       if (side != viewer.getSide()) return;
378       Project project = ObjectUtils.assertNotNull(viewer.getProject());
379       AnnotateToggleAction.doAnnotate(viewer.getEditor(), project, null, data.annotation, data.vcs, null);
380     }
381   }
382
383   private static class UnifiedAnnotator extends ViewerAnnotator<UnifiedDiffViewer> {
384     @Override
385     @NotNull
386     public Class<UnifiedDiffViewer> getViewerClass() {
387       return UnifiedDiffViewer.class;
388     }
389
390     @Override
391     @Nullable
392     public Side getCurrentSide(@NotNull UnifiedDiffViewer viewer, @NotNull Editor editor) {
393       if (viewer.getEditor() != editor) return null;
394       return viewer.getMasterSide();
395     }
396
397     @Override
398     public boolean isAnnotationShown(@NotNull UnifiedDiffViewer viewer, @NotNull Side side) {
399       if (side != viewer.getMasterSide()) return false;
400       return viewer.getEditor().getGutter().isAnnotationsShown();
401     }
402
403     @Override
404     public void showAnnotation(@NotNull UnifiedDiffViewer viewer, @NotNull Side side, @NotNull AnnotationData data) {
405       if (side != viewer.getMasterSide()) return;
406       Project project = ObjectUtils.assertNotNull(viewer.getProject());
407       UnifiedUpToDateLineNumberProvider lineNumberProvider = new UnifiedUpToDateLineNumberProvider(viewer, side);
408       AnnotateToggleAction.doAnnotate(viewer.getEditor(), project, null, data.annotation, data.vcs, lineNumberProvider);
409     }
410   }
411
412   private static class UnifiedUpToDateLineNumberProvider implements UpToDateLineNumberProvider {
413     @NotNull private final UnifiedDiffViewer myViewer;
414     @NotNull private final Side mySide;
415     @NotNull private final UpToDateLineNumberProvider myLocalChangesProvider;
416
417     public UnifiedUpToDateLineNumberProvider(@NotNull UnifiedDiffViewer viewer, @NotNull Side side) {
418       myViewer = viewer;
419       mySide = side;
420       myLocalChangesProvider = new UpToDateLineNumberProviderImpl(myViewer.getDocument(mySide), viewer.getProject());
421     }
422
423     @Override
424     public int getLineNumber(int currentNumber) {
425       int number = myViewer.transferLineFromOnesideStrict(mySide, currentNumber);
426       return number != -1 ? myLocalChangesProvider.getLineNumber(number) : -1;
427     }
428
429     @Override
430     public boolean isLineChanged(int currentNumber) {
431       return getLineNumber(currentNumber) == -1;
432     }
433
434     @Override
435     public boolean isRangeChanged(int start, int end) {
436       int line1 = myViewer.transferLineFromOnesideStrict(mySide, start);
437       int line2 = myViewer.transferLineFromOnesideStrict(mySide, end);
438       if (line2 - line1 != end - start) return true;
439
440       for (int i = start; i <= end; i++) {
441         if (isLineChanged(i)) return true; // TODO: a single request to LineNumberConvertor
442       }
443       return myLocalChangesProvider.isRangeChanged(line1, line2);
444     }
445   }
446
447   private static abstract class ViewerAnnotator<T extends DiffViewerBase> {
448     @NotNull
449     public abstract Class<T> getViewerClass();
450
451     @Nullable
452     public abstract Side getCurrentSide(@NotNull T viewer, @NotNull Editor editor);
453
454     public abstract boolean isAnnotationShown(@NotNull T viewer, @NotNull Side side);
455
456     public abstract void showAnnotation(@NotNull T viewer, @NotNull Side side, @NotNull AnnotationData data);
457   }
458
459   private abstract static class FileAnnotationLoader {
460     @NotNull private final AbstractVcs myVcs;
461     private final boolean myShouldCache;
462
463     private VcsException myException;
464     private FileAnnotation myResult;
465
466     public FileAnnotationLoader(@NotNull AbstractVcs vcs, boolean cache) {
467       myVcs = vcs;
468       myShouldCache = cache;
469     }
470
471     public VcsException getException() {
472       return myException;
473     }
474
475     public AnnotationData getResult() {
476       return new AnnotationData(myVcs, myResult);
477     }
478
479     public boolean shouldCache() {
480       return myShouldCache;
481     }
482
483     public void run() {
484       try {
485         myResult = compute();
486       }
487       catch (VcsException e) {
488         myException = e;
489       }
490     }
491
492     protected abstract FileAnnotation compute() throws VcsException;
493   }
494
495   private static class AnnotationData {
496     @NotNull public final AbstractVcs vcs;
497     @NotNull public final FileAnnotation annotation;
498
499     public AnnotationData(@NotNull AbstractVcs vcs, @NotNull FileAnnotation annotation) {
500       this.vcs = vcs;
501       this.annotation = annotation;
502     }
503   }
504
505   private static boolean checkRunningProgress(@NotNull DiffViewerBase viewer, @NotNull Side side) {
506     final ProjectLevelVcsManagerImpl plVcsManager = (ProjectLevelVcsManagerImpl)ProjectLevelVcsManager.getInstance(viewer.getProject());
507     final BackgroundableActionEnabledHandler handler = plVcsManager.getBackgroundableActionHandler(VcsBackgroundableActions.ANNOTATE);
508     return handler.isInProgress(key(viewer, side));
509   }
510
511   private static void markRunningProgress(@NotNull DiffViewerBase viewer, @NotNull Side side, boolean running) {
512     final ProjectLevelVcsManagerImpl plVcsManager = (ProjectLevelVcsManagerImpl)ProjectLevelVcsManager.getInstance(viewer.getProject());
513     final BackgroundableActionEnabledHandler handler = plVcsManager.getBackgroundableActionHandler(VcsBackgroundableActions.ANNOTATE);
514     if (running) {
515       handler.register(key(viewer, side));
516     }
517     else {
518       handler.completed(key(viewer, side));
519     }
520   }
521
522   @NotNull
523   private static Object key(@NotNull DiffViewer viewer, @NotNull Side side) {
524     return Pair.create(viewer, side);
525   }
526 }