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