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