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