50768a3f14a13dee21dcfde6108702646b52df6c
[idea/community.git] / platform / vcs-impl / src / com / intellij / openapi / vcs / actions / AnnotateToggleAction.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.openapi.actionSystem.AnAction;
19 import com.intellij.openapi.actionSystem.AnActionEvent;
20 import com.intellij.openapi.actionSystem.Separator;
21 import com.intellij.openapi.actionSystem.ToggleAction;
22 import com.intellij.openapi.diagnostic.Logger;
23 import com.intellij.openapi.editor.Editor;
24 import com.intellij.openapi.editor.ex.EditorGutterComponentEx;
25 import com.intellij.openapi.fileEditor.FileDocumentManager;
26 import com.intellij.openapi.fileEditor.FileEditor;
27 import com.intellij.openapi.fileEditor.FileEditorManager;
28 import com.intellij.openapi.fileEditor.TextEditor;
29 import com.intellij.openapi.localVcs.UpToDateLineNumberProvider;
30 import com.intellij.openapi.progress.ProgressIndicator;
31 import com.intellij.openapi.progress.ProgressManager;
32 import com.intellij.openapi.progress.Task;
33 import com.intellij.openapi.project.DumbAware;
34 import com.intellij.openapi.project.Project;
35 import com.intellij.openapi.util.Couple;
36 import com.intellij.openapi.util.Ref;
37 import com.intellij.openapi.util.registry.Registry;
38 import com.intellij.openapi.vcs.*;
39 import com.intellij.openapi.vcs.annotate.*;
40 import com.intellij.openapi.vcs.changes.BackgroundFromStartOption;
41 import com.intellij.openapi.vcs.history.VcsFileRevision;
42 import com.intellij.openapi.vcs.history.VcsRevisionNumber;
43 import com.intellij.openapi.vcs.impl.BackgroundableActionEnabledHandler;
44 import com.intellij.openapi.vcs.impl.ProjectLevelVcsManagerImpl;
45 import com.intellij.openapi.vcs.impl.UpToDateLineNumberProviderImpl;
46 import com.intellij.openapi.vcs.impl.VcsBackgroundableActions;
47 import com.intellij.openapi.vfs.VirtualFile;
48 import com.intellij.util.ui.UIUtil;
49 import org.jetbrains.annotations.NotNull;
50 import org.jetbrains.annotations.Nullable;
51
52 import java.awt.*;
53 import java.util.*;
54 import java.util.List;
55
56 /**
57  * @author Konstantin Bulenkov
58  * @author: lesya
59  */
60 public class AnnotateToggleAction extends ToggleAction implements DumbAware, AnnotationColors {
61   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vcs.actions.AnnotateToggleAction");
62
63   @Override
64   public void update(@NotNull AnActionEvent e) {
65     super.update(e);
66     final boolean enabled = isEnabled(VcsContextFactory.SERVICE.getInstance().createContextOn(e));
67     e.getPresentation().setEnabled(enabled);
68   }
69
70   private static boolean isEnabled(final VcsContext context) {
71     VirtualFile[] selectedFiles = context.getSelectedFiles();
72     if (selectedFiles.length != 1) {
73       return false;
74     }
75     VirtualFile file = selectedFiles[0];
76     if (file.isDirectory()) return false;
77     Project project = context.getProject();
78     if (project == null || project.isDisposed()) return false;
79
80     final ProjectLevelVcsManager plVcsManager = ProjectLevelVcsManager.getInstance(project);
81     final BackgroundableActionEnabledHandler handler = ((ProjectLevelVcsManagerImpl)plVcsManager)
82       .getBackgroundableActionHandler(VcsBackgroundableActions.ANNOTATE);
83     if (handler.isInProgress(file.getPath())) return false;
84
85     final AbstractVcs vcs = plVcsManager.getVcsFor(file);
86     if (vcs == null) return false;
87     final AnnotationProvider annotationProvider = vcs.getAnnotationProvider();
88     if (annotationProvider == null) return false;
89     final FileStatus fileStatus = FileStatusManager.getInstance(project).getStatus(file);
90     if (fileStatus == FileStatus.UNKNOWN || fileStatus == FileStatus.ADDED || fileStatus == FileStatus.IGNORED) {
91       return false;
92     }
93     return hasTextEditor(file);
94   }
95
96   private static boolean hasTextEditor(@NotNull VirtualFile selectedFile) {
97     return !selectedFile.getFileType().isBinary();
98   }
99
100   @Override
101   public boolean isSelected(AnActionEvent e) {
102     VcsContext context = VcsContextFactory.SERVICE.getInstance().createContextOn(e);
103     Editor editor = context.getEditor();
104     if (editor != null) {
105       return isAnnotated(editor);
106     }
107     VirtualFile selectedFile = context.getSelectedFile();
108     if (selectedFile == null) {
109       return false;
110     }
111
112     Project project = context.getProject();
113     if (project == null) return false;
114
115     for (FileEditor fileEditor : FileEditorManager.getInstance(project).getEditors(selectedFile)) {
116       if (fileEditor instanceof TextEditor) {
117         if (isAnnotated(((TextEditor)fileEditor).getEditor())) {
118           return true;
119         }
120       }
121     }
122     return false;
123   }
124
125   private static boolean isAnnotated(@NotNull Editor editor) {
126     return editor.getGutter().isAnnotationsShown();
127   }
128
129   @Override
130   public void setSelected(AnActionEvent e, boolean selected) {
131     final VcsContext context = VcsContextFactory.SERVICE.getInstance().createContextOn(e);
132     Editor editor = context.getEditor();
133     VirtualFile selectedFile = context.getSelectedFile();
134     if (selectedFile == null) return;
135
136     Project project = context.getProject();
137     if (project == null) return;
138     if (!selected) {
139       for (FileEditor fileEditor : FileEditorManager.getInstance(project).getEditors(selectedFile)) {
140         if (fileEditor instanceof TextEditor) {
141           ((TextEditor)fileEditor).getEditor().getGutter().closeAllAnnotations();
142         }
143       }
144     }
145     else {
146       if (editor == null) {
147         FileEditor[] fileEditors = FileEditorManager.getInstance(project).openFile(selectedFile, false);
148         for (FileEditor fileEditor : fileEditors) {
149           if (fileEditor instanceof TextEditor) {
150             editor = ((TextEditor)fileEditor).getEditor();
151           }
152         }
153       }
154       LOG.assertTrue(editor != null);
155       doAnnotate(editor, project);
156     }
157   }
158
159   private static void doAnnotate(final Editor editor, final Project project) {
160     final VirtualFile file = FileDocumentManager.getInstance().getFile(editor.getDocument());
161     if (project == null || file == null) {
162       return;
163     }
164     final ProjectLevelVcsManager plVcsManager = ProjectLevelVcsManager.getInstance(project);
165     final AbstractVcs vcs = plVcsManager.getVcsFor(file);
166
167     if (vcs == null) return;
168
169     final AnnotationProvider annotationProvider = vcs.getCachingAnnotationProvider();
170     assert annotationProvider != null;
171
172     final Ref<FileAnnotation> fileAnnotationRef = new Ref<FileAnnotation>();
173     final Ref<VcsException> exceptionRef = new Ref<VcsException>();
174
175     final BackgroundableActionEnabledHandler handler = ((ProjectLevelVcsManagerImpl)plVcsManager).getBackgroundableActionHandler(
176       VcsBackgroundableActions.ANNOTATE);
177     handler.register(file.getPath());
178
179     final Task.Backgroundable annotateTask = new Task.Backgroundable(project,
180                                                                      VcsBundle.message("retrieving.annotations"),
181                                                                      true,
182                                                                      BackgroundFromStartOption.getInstance()) {
183       @Override
184       public void run(final @NotNull ProgressIndicator indicator) {
185         try {
186           fileAnnotationRef.set(annotationProvider.annotate(file));
187         }
188         catch (VcsException e) {
189           exceptionRef.set(e);
190         }
191         catch (Throwable t) {
192           exceptionRef.set(new VcsException(t));
193         }
194       }
195
196       @Override
197       public void onCancel() {
198         onSuccess();
199       }
200
201       @Override
202       public void onSuccess() {
203         handler.completed(file.getPath());
204
205         if (!exceptionRef.isNull()) {
206           LOG.warn(exceptionRef.get());
207           AbstractVcsHelper.getInstance(project).showErrors(Collections.singletonList(exceptionRef.get()), VcsBundle.message("message.title.annotate"));
208         }
209
210         if (!fileAnnotationRef.isNull()) {
211           doAnnotate(editor, project, file, fileAnnotationRef.get(), vcs, true);
212         }
213       }
214     };
215     ProgressManager.getInstance().run(annotateTask);
216   }
217
218   public static void doAnnotate(@NotNull final Editor editor,
219                                 @NotNull final Project project,
220                                 @NotNull final VirtualFile currentFile,
221                                 @NotNull final FileAnnotation fileAnnotation,
222                                 @NotNull final AbstractVcs vcs,
223                                 final boolean onCurrentRevision) {
224     editor.getGutter().closeAllAnnotations();
225
226     fileAnnotation.setCloser(new Runnable() {
227       @Override
228       public void run() {
229         if (project.isDisposed()) return;
230         UIUtil.invokeLaterIfNeeded(new Runnable() {
231           @Override
232           public void run() {
233             if (project.isDisposed()) return;
234             editor.getGutter().closeAllAnnotations();
235           }
236         });
237       }
238     });
239     if (onCurrentRevision) {
240       ProjectLevelVcsManager.getInstance(project).getAnnotationLocalChangesListener().registerAnnotation(fileAnnotation.getFile(), fileAnnotation);
241     }
242
243     final EditorGutterComponentEx editorGutter = (EditorGutterComponentEx)editor.getGutter();
244     final List<AnnotationFieldGutter> gutters = new ArrayList<AnnotationFieldGutter>();
245     final AnnotationSourceSwitcher switcher = fileAnnotation.getAnnotationSourceSwitcher();
246     final UpToDateLineNumberProvider getUpToDateLineNumber = new UpToDateLineNumberProviderImpl(editor.getDocument(), project);
247
248     final AnnotationPresentation presentation = new AnnotationPresentation(fileAnnotation, getUpToDateLineNumber, switcher);
249     if (vcs.getCommittedChangesProvider() != null) {
250       presentation.addAction(new ShowDiffFromAnnotation(fileAnnotation, vcs, currentFile));
251     }
252     presentation.addAction(new CopyRevisionNumberFromAnnotateAction(fileAnnotation));
253     presentation.addAction(Separator.getInstance());
254
255     final Couple<Map<VcsRevisionNumber, Color>> bgColorMap =
256       Registry.is("vcs.show.colored.annotations") ? computeBgColors(fileAnnotation) : null;
257     final Map<VcsRevisionNumber, Integer> historyIds = Registry.is("vcs.show.history.numbers") ? computeLineNumbers(fileAnnotation) : null;
258
259     if (switcher != null) {
260       switcher.switchTo(switcher.getDefaultSource());
261       final LineAnnotationAspect revisionAspect = switcher.getRevisionAspect();
262       final CurrentRevisionAnnotationFieldGutter currentRevisionGutter =
263         new CurrentRevisionAnnotationFieldGutter(fileAnnotation, revisionAspect, presentation, bgColorMap);
264       final MergeSourceAvailableMarkerGutter mergeSourceGutter =
265         new MergeSourceAvailableMarkerGutter(fileAnnotation, null, presentation, bgColorMap);
266
267       presentation.addAction(new SwitchAnnotationSourceAction(switcher, editorGutter));
268       presentation.addSourceSwitchListener(currentRevisionGutter);
269       presentation.addSourceSwitchListener(mergeSourceGutter);
270
271       currentRevisionGutter.consume(switcher.getDefaultSource());
272       mergeSourceGutter.consume(switcher.getDefaultSource());
273
274       gutters.add(currentRevisionGutter);
275       gutters.add(mergeSourceGutter);
276     }
277
278     final LineAnnotationAspect[] aspects = fileAnnotation.getAspects();
279     for (LineAnnotationAspect aspect : aspects) {
280       gutters.add(new AnnotationFieldGutter(fileAnnotation, aspect, presentation, bgColorMap));
281     }
282
283
284     if (historyIds != null) {
285       gutters.add(new HistoryIdColumn(fileAnnotation, presentation, bgColorMap, historyIds));
286     }
287     gutters.add(new HighlightedAdditionalColumn(fileAnnotation, null, presentation, bgColorMap));
288     final AnnotateActionGroup actionGroup = new AnnotateActionGroup(gutters, editorGutter);
289     presentation.addAction(actionGroup, 1);
290     gutters.add(new ExtraFieldGutter(fileAnnotation, presentation, bgColorMap, actionGroup));
291
292     presentation.addAction(new AnnotateCurrentRevisionAction(fileAnnotation, vcs));
293     presentation.addAction(new AnnotatePreviousRevisionAction(fileAnnotation, vcs));
294     addActionsFromExtensions(presentation, fileAnnotation);
295
296     for (AnnotationFieldGutter gutter : gutters) {
297       final AnnotationGutterLineConvertorProxy proxy = new AnnotationGutterLineConvertorProxy(getUpToDateLineNumber, gutter);
298       if (gutter.isGutterAction()) {
299         editor.getGutter().registerTextAnnotation(proxy, proxy);
300       }
301       else {
302         editor.getGutter().registerTextAnnotation(proxy);
303       }
304     }
305   }
306
307   private static void addActionsFromExtensions(@NotNull AnnotationPresentation presentation, @NotNull FileAnnotation fileAnnotation) {
308     AnnotationGutterActionProvider[] extensions = AnnotationGutterActionProvider.EP_NAME.getExtensions();
309     if (extensions.length > 0) {
310       presentation.addAction(new Separator());
311     }
312     for (AnnotationGutterActionProvider provider : extensions) {
313       presentation.addAction(provider.createAction(fileAnnotation));
314     }
315   }
316
317   @Nullable
318   private static Map<VcsRevisionNumber, Integer> computeLineNumbers(@NotNull FileAnnotation fileAnnotation) {
319     final Map<VcsRevisionNumber, Integer> numbers = new HashMap<VcsRevisionNumber, Integer>();
320     final List<VcsFileRevision> fileRevisionList = fileAnnotation.getRevisions();
321     if (fileRevisionList != null) {
322       int size = fileRevisionList.size();
323       for (int i = 0; i < size; i++) {
324         VcsFileRevision revision = fileRevisionList.get(i);
325         final VcsRevisionNumber number = revision.getRevisionNumber();
326
327         numbers.put(number, size - i);
328       }
329     }
330     return numbers.size() < 2 ? null : numbers;
331   }
332
333   @NotNull
334   private static Couple<Map<VcsRevisionNumber, Color>> computeBgColors(@NotNull FileAnnotation fileAnnotation) {
335     final Map<VcsRevisionNumber, Color> commitOrderColors = new HashMap<VcsRevisionNumber, Color>();
336     final Map<VcsRevisionNumber, Color> commitAuthorColors = new HashMap<VcsRevisionNumber, Color>();
337     final Map<String, Color> authorColors = new HashMap<String, Color>();
338     final List<VcsFileRevision> fileRevisionList = fileAnnotation.getRevisions();
339     if (fileRevisionList != null) {
340       final int colorsCount = BG_COLORS.length;
341       final int revisionsCount = fileRevisionList.size();
342
343       for (int i = 0; i < fileRevisionList.size(); i++) {
344         VcsFileRevision revision = fileRevisionList.get(i);
345         final VcsRevisionNumber number = revision.getRevisionNumber();
346         final String author = revision.getAuthor();
347         if (number == null) continue;
348
349         if (!commitAuthorColors.containsKey(number)) {
350           if (author != null && !authorColors.containsKey(author)) {
351             final int index = authorColors.size();
352             Color color = BG_COLORS[index * BG_COLORS_PRIME % colorsCount];
353             authorColors.put(author, color);
354           }
355
356           commitAuthorColors.put(number, authorColors.get(author));
357         }
358         if (!commitOrderColors.containsKey(number)) {
359           Color color = BG_COLORS[colorsCount * i / revisionsCount];
360           commitOrderColors.put(number, color);
361         }
362       }
363     }
364     return Couple.of(commitOrderColors.size() > 1 ? commitOrderColors : null,
365                      commitAuthorColors.size() > 1 ? commitAuthorColors : null);
366   }
367 }